En utilisant le CTP async de Microsoft pour .NET, est-il possible d’attraper une exception levée par une méthode async dans la méthode d’appel?
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. */
}
}
Je souhaite donc que l’exception du code asynchrone soit intégrée dans mon code d’appel, si cela est possible.
C'est un peu bizarre à lire mais oui, l'exception va remonter jusqu'au code de l'appelant - mais seulement si vous await
ou Wait()
l'appel de Foo
.
public async Task Foo()
{
var x = await DoSomethingAsync();
}
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 void async ont différentes sémantiques de traitement des erreurs. Lorsqu'une exception est rejetée à l'aide 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 void async, il n'y a pas d'objet Task, donc toute exception levée d'une méthode void async sera levée directement sur le SynchronizationContext qui était actif au démarrage de la méthode void async. - https://msdn.Microsoft.com/en-us/magazine/jj991977.aspx
Notez que l'utilisation de Wait () peut entraîner 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 très bonne - elle décrit les étapes que le compilateur prend pour y parvenir. la magie.
La raison pour laquelle l'exception n'est pas interceptée est que la méthode Foo () a un type de retour vide et que, dès que wait est appelée, elle retourne simplement. Comme DoFoo () n'attend pas l'achèvement de Foo, le gestionnaire d'exceptions ne peut pas être utilisé.
Cela ouvre une solution plus simple si vous pouvez modifier les signatures de la méthode - modifiez Foo()
pour qu'il renvoie le type Task
, puis DoFoo()
pouvez 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 sont renvoyées immédiatement après le début de l'attente du résultat asynchrone. Il est judicieux d'utiliser le traçage afin de déterminer le comportement réel du code.
Le code ci-dessous fait ce qui suit:
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;
}
Quand on observe 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 une méthode try/catch autour de votre méthode wait, 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 suit automatiquement l’exception levée car j’ai utilisé ApiChange.Api.dll à partir de l’outil ApiChange . Traçage et réflecteur aide beaucoup à comprendre ce qui se passe. Pour vous débarrasser des threads, vous pouvez créer vos propres versions de GetAwaiter BeginAwait et EndAwait et envelopper non pas une tâche, mais par exemple. Paresseux et tracez à l'intérieur de vos propres méthodes d'extension. Vous comprendrez alors beaucoup mieux ce que le compilateur et ce que fait la TPL.
Vous voyez maintenant qu'il n'y a aucun moyen de récupérer/essayer votre exception car il ne reste plus de cadre de pile dans lequel propager une exception. Votre code est peut-être en train de faire quelque chose de totalement différent après avoir lancé les opérations asynchrones. Il pourrait appeler Thread.Sleep ou même se terminer. Tant qu'il reste un fil de premier plan, votre application continuera volontiers à exécuter des tâches asynchrones.
Vous pouvez gérer l'exception à l'intérieur de la méthode asynchrone une fois votre opération asynchrone terminée et rappeler dans le thread d'interface utilisateur. La méthode recommandée consiste à utiliser TaskScheduler.FromSynchronizationContext . Cela ne fonctionne que si vous avez un thread d'interface utilisateur et qu'il n'est pas très occupé par d'autres tâches.
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();
}
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 vide sur une méthode asynchrone. Je recommanderais de retourner la tâche comme suit. Cela facilitera grandement le débogage.
public async Task DoFoo()
{
try
{
return await Foo();
}
catch (ProtocolException ex)
{
/* Exception with chronological stack trace */
}
}
Ce blog explique parfaitement votre problème Bonnes pratiques asynchrones .
L'essentiel étant que vous ne devriez pas utiliser void comme retour pour une méthode async, à moins que ce ne soit un gestionnaire d'événements async, c'est une mauvaise pratique car cela ne permet pas d'attraper des exceptions ;-).
La meilleure pratique serait de changer le type de retour en tâche. Essayez également de coder en async tout au long du processus, de lancer chaque appel de méthode asynchrone et d’être appelé à partir de méthodes asynchrones. À l'exception d'une méthode Main dans une console, qui ne peut pas être asynchrone (avant C # 7.1).
Si vous ignorez cette pratique recommandée, vous rencontrerez des blocages avec des applications à interface graphique et ASP.NET. Le blocage est dû au fait que ces applications s'exécutent sur un contexte qui n'autorise qu'un seul thread et ne le cède pas au thread async. Cela signifie que l'interface graphique attend le retour de manière synchrone, tandis que la méthode asynchrone attend le contexte: blocage.
Ce problème ne se produira pas dans une application console, car elle s'exécute sur un contexte avec un pool de threads. La méthode async retournera sur un autre thread qui sera planifié. C'est pourquoi une application de console de test fonctionnera, mais les mêmes appels se verrouillent dans d'autres applications ...