Intercepter une exception levée par une méthode asynchrone
Utilisation du CTP asynchrone de Microsoft pour. NET, est-il possible d'attraper une exception levée par une méthode asynchrone dans la méthode appelante?
public async void Foo()
{
var x = await DoSomethingAsync();
/* Handle the result, but sometimes an exception might be thrown.
For example, DoSomethingAsync gets data from the network
and the data is invalid... a ProtocolException might be thrown. */
}
public void DoFoo()
{
try
{
Foo();
}
catch (ProtocolException ex)
{
/* The exception will never be caught.
Instead when in debug mode, VS2010 will warn and continue.
The deployed the app will simply crash. */
}
}
Donc, fondamentalement, je veux que l'exception du code asynchrone fasse des bulles dans mon code appelant si cela est même possible du tout.
6 réponses
C'est un peu bizarre à lire, mais oui, l'exception de la bulle jusqu'à l'appel de code, mais seulement si vous await
ou Wait()
l'appel à Foo
.
public async void DoFoo()
{
try
{
await Foo();
}
catch (ProtocolException ex)
{
// The exception will be caught because you've awaited
// the call in an async method.
}
}
//or//
public void DoFoo()
{
try
{
Foo().Wait();
}
catch (ProtocolException ex)
{
/* The exception will be caught because you've
waited for the completion of the call. */
}
}
Les méthodes asynchrones void ont une sémantique de gestion des erreurs différente. Lorsqu'une exception est levée d'une tâche asynchrone ou d'une méthode de tâche asynchrone, cette exception est capturée et placée sur l'objet Task. Avec les méthodes asynchrones void, il n'y a pas D'objet Task, donc toutes les exceptions rejetées par une méthode asynchrone void seront déclenchées directement sur le SynchronizationContext qui était actif lorsque la méthode async void a démarré. - https://msdn.microsoft.com/en-us/magazine/jj991977.aspx
Notez que l'utilisation de Wait () peut provoquer le blocage de votre application, si.Net décide d'exécuter votre méthode de manière synchrone.
Cette explication http://www.interact-sw.co.uk/iangblog/2010/11/01/csharp5-async-exceptions est assez bon - il discute des étapes que le compilateur prend pour réaliser cette magie.
La raison pour laquelle l'exception n'est pas interceptée est que la méthode Foo () a un type de retour void et donc quand await est appelée, elle retourne simplement. Comme DoFoo () n'attend pas la fin de Foo, le gestionnaire d'exceptions ne peut pas être utilisé.
, Cela ouvre une solution plus simple si vous pouvez modifier les signatures de méthode - alter Foo()
, de sorte qu'il renvoie type Task
, puis DoFoo()
peut await Foo()
, comme dans ce code:
public async Task Foo() {
var x = await DoSomethingThatThrows();
}
public async void DoFoo() {
try {
await Foo();
} catch (ProtocolException ex) {
// This will catch exceptions from DoSomethingThatThrows
}
}
Votre code ne fait pas ce que vous pourriez penser. Les méthodes asynchrones reviennent immédiatement après que la méthode commence à attendre le résultat asynchrone. Il est perspicace d'utiliser le traçage afin d'étudier comment le code se comporte réellement.
Le code ci-dessous fait ce qui suit:
- Créer 4 tâches
- chaque tâche incrémente de manière asynchrone un nombre et renvoie le nombre incrémenté
- lorsque le résultat asynchrone est arrivé, il est tracer.
static TypeHashes _type = new TypeHashes(typeof(Program));
private void Run()
{
TracerConfig.Reset("debugoutput");
using (Tracer t = new Tracer(_type, "Run"))
{
for (int i = 0; i < 4; i++)
{
DoSomeThingAsync(i);
}
}
Application.Run(); // Start window message pump to prevent termination
}
private async void DoSomeThingAsync(int i)
{
using (Tracer t = new Tracer(_type, "DoSomeThingAsync"))
{
t.Info("Hi in DoSomething {0}",i);
try
{
int result = await Calculate(i);
t.Info("Got async result: {0}", result);
}
catch (ArgumentException ex)
{
t.Error("Got argument exception: {0}", ex);
}
}
}
Task<int> Calculate(int i)
{
var t = new Task<int>(() =>
{
using (Tracer t2 = new Tracer(_type, "Calculate"))
{
if( i % 2 == 0 )
throw new ArgumentException(String.Format("Even argument {0}", i));
return i++;
}
});
t.Start();
return t;
}
Lorsque vous observez les traces
22:25:12.649 02172/02820 { AsyncTest.Program.Run
22:25:12.656 02172/02820 { AsyncTest.Program.DoSomeThingAsync
22:25:12.657 02172/02820 Information AsyncTest.Program.DoSomeThingAsync Hi in DoSomething 0
22:25:12.658 02172/05220 { AsyncTest.Program.Calculate
22:25:12.659 02172/02820 { AsyncTest.Program.DoSomeThingAsync
22:25:12.659 02172/02820 Information AsyncTest.Program.DoSomeThingAsync Hi in DoSomething 1
22:25:12.660 02172/02756 { AsyncTest.Program.Calculate
22:25:12.662 02172/02820 { AsyncTest.Program.DoSomeThingAsync
22:25:12.662 02172/02820 Information AsyncTest.Program.DoSomeThingAsync Hi in DoSomething 2
22:25:12.662 02172/02820 { AsyncTest.Program.DoSomeThingAsync
22:25:12.662 02172/02820 Information AsyncTest.Program.DoSomeThingAsync Hi in DoSomething 3
22:25:12.664 02172/02756 } AsyncTest.Program.Calculate Duration 4ms
22:25:12.666 02172/02820 } AsyncTest.Program.Run Duration 17ms ---- Run has completed. The async methods are now scheduled on different threads.
22:25:12.667 02172/02756 Information AsyncTest.Program.DoSomeThingAsync Got async result: 1
22:25:12.667 02172/02756 } AsyncTest.Program.DoSomeThingAsync Duration 8ms
22:25:12.667 02172/02756 { AsyncTest.Program.Calculate
22:25:12.665 02172/05220 Exception AsyncTest.Program.Calculate Exception thrown: System.ArgumentException: Even argument 0
at AsyncTest.Program.c__DisplayClassf.Calculateb__e() in C:\Source\AsyncTest\AsyncTest\Program.cs:line 124
at System.Threading.Tasks.Task`1.InvokeFuture(Object futureAsObj)
at System.Threading.Tasks.Task.InnerInvoke()
at System.Threading.Tasks.Task.Execute()
22:25:12.668 02172/02756 Exception AsyncTest.Program.Calculate Exception thrown: System.ArgumentException: Even argument 2
at AsyncTest.Program.c__DisplayClassf.Calculateb__e() in C:\Source\AsyncTest\AsyncTest\Program.cs:line 124
at System.Threading.Tasks.Task`1.InvokeFuture(Object futureAsObj)
at System.Threading.Tasks.Task.InnerInvoke()
at System.Threading.Tasks.Task.Execute()
22:25:12.724 02172/05220 } AsyncTest.Program.Calculate Duration 66ms
22:25:12.724 02172/02756 } AsyncTest.Program.Calculate Duration 57ms
22:25:12.725 02172/05220 Error AsyncTest.Program.DoSomeThingAsync Got argument exception: System.ArgumentException: Even argument 0
Server stack trace:
at AsyncTest.Program.c__DisplayClassf.Calculateb__e() in C:\Source\AsyncTest\AsyncTest\Program.cs:line 124
at System.Threading.Tasks.Task`1.InvokeFuture(Object futureAsObj)
at System.Threading.Tasks.Task.InnerInvoke()
at System.Threading.Tasks.Task.Execute()
Exception rethrown at [0]:
at System.Runtime.CompilerServices.TaskAwaiter.EndAwait()
at System.Runtime.CompilerServices.TaskAwaiter`1.EndAwait()
at AsyncTest.Program.DoSomeThingAsyncd__8.MoveNext() in C:\Source\AsyncTest\AsyncTest\Program.cs:line 106
22:25:12.725 02172/02756 Error AsyncTest.Program.DoSomeThingAsync Got argument exception: System.ArgumentException: Even argument 2
Server stack trace:
at AsyncTest.Program.c__DisplayClassf.Calculateb__e() in C:\Source\AsyncTest\AsyncTest\Program.cs:line 124
at System.Threading.Tasks.Task`1.InvokeFuture(Object futureAsObj)
at System.Threading.Tasks.Task.InnerInvoke()
at System.Threading.Tasks.Task.Execute()
Exception rethrown at [0]:
at System.Runtime.CompilerServices.TaskAwaiter.EndAwait()
at System.Runtime.CompilerServices.TaskAwaiter`1.EndAwait()
at AsyncTest.Program.DoSomeThingAsyncd__8.MoveNext() in C:\Source\AsyncTest\AsyncTest\Program.cs:line 0
22:25:12.726 02172/05220 } AsyncTest.Program.DoSomeThingAsync Duration 70ms
22:25:12.726 02172/02756 } AsyncTest.Program.DoSomeThingAsync Duration 64ms
22:25:12.726 02172/05220 { AsyncTest.Program.Calculate
22:25:12.726 02172/05220 } AsyncTest.Program.Calculate Duration 0ms
22:25:12.726 02172/05220 Information AsyncTest.Program.DoSomeThingAsync Got async result: 3
22:25:12.726 02172/05220 } AsyncTest.Program.DoSomeThingAsync Duration 64ms
Vous remarquerez que la méthode Run se termine sur le thread 2820 alors qu'un seul thread enfant est terminé (2756). Si vous mettez un try / catch autour de votre méthode await, vous pouvez "attraper" l'exception de la manière habituelle bien que votre code soit exécuté sur un autre thread lorsque la tâche de calcul est terminée et que votre contiuation est exécutée.
La méthode de calcul trace automatiquement l'exception levée parce que j'ai utilisé L'ApiChange.API.dll de l'outilApiChange . Le traçage et le réflecteur aident beaucoup à comprendre ce qui se passe. Pour vous débarrasser du threading, vous pouvez créer vos propres versions de GetAwaiter BeginAwait et EndAwait et envelopper non pas une tâche mais par exemple un paresseux et tracer dans vos propres méthodes d'extension. Ensuite, vous comprendrez beaucoup mieux ce que fait le compilateur et ce que fait le TPL.
Maintenant, vous voyez qu'il n'y a aucun moyen d'essayer / de récupérer votre exception depuis il n'y a plus de cadre de pile pour une exception à propager. Votre code pourrait faire quelque chose de totalement différent après avoir lancé les opérations asynchrones. Il pourrait appeler Thread.Dormir ou même se terminer. Tant qu'il reste un thread de premier plan, votre application continuera volontiers à exécuter des tâches asynchrones.
Vous pouvez gérer l'exception dans la méthode asynchrone après la fin de votre opération asynchrone et rappeler dans le thread de L'interface utilisateur. La manière recommandée pour ce faire est avec TaskScheduler.FromSynchronizationContext. Qui fonctionne seulement si vous avez un thread d'INTERFACE utilisateur et il n'est pas très occupé avec d'autres choses.
Il est également important de noter que vous perdrez la trace chronologique de la pile de l'exception si vous avez un type de retour void sur une méthode asynchrone. Je recommanderais de retourner la tâche comme suit. Va rendre le débogage beaucoup plus facile.
public async Task DoFoo()
{
try
{
return await Foo();
}
catch (ProtocolException ex)
{
/* Exception with chronological stack trace */
}
}
L'exception peut être interceptée dans la fonction asynchrone.
public async void Foo()
{
try
{
var x = await DoSomethingAsync();
/* Handle the result, but sometimes an exception might be thrown
For example, DoSomethingAsync get's data from the network
and the data is invalid... a ProtocolException might be thrown */
}
catch (ProtocolException ex)
{
/* The exception will be caught here */
}
}
public void DoFoo()
{
Foo();
}
Ce blog explique parfaitement votre problème meilleures pratiques asynchrones .
L'essentiel étant que vous ne devriez pas utiliser void comme retour pour une méthode asynchrone, sauf s'il s'agit d'un gestionnaire d'événements asynchrone, c'est une mauvaise pratique car cela ne permet pas d'intercepter des exceptions ;-).
La meilleure pratique serait de changer le type de retour en tâche. En outre, essayez de coder async tout le chemin, faites chaque appel de méthode asynchrone et soyez appelé à partir de méthodes asynchrones. Sauf pour une méthode Principale dans une console, qui ne peut pas être asynchrone (avant c # 7.1).
Vous rencontrerez des blocages avec L'interface graphique et ASP.NET applications Si vous ignorez cette meilleure pratique. Le blocage se produit parce que ces applications s'exécutent sur un contexte qui n'autorise qu'un seul thread et ne le renoncera pas au thread asynchrone. Cela signifie que l'interface graphique attend de manière synchrone un retour, tandis que la méthode asynchrone attend le contexte: deadlock.
Ce comportement ne se produira pas dans une application console, car il s'exécute en contexte avec un pool de threads. La méthode asynchrone retournera sur un autre thread qui sera planifié. C'est pourquoi une application de console de test fonctionnera, mais les mêmes appels seront bloqués dans d'autres applications...