web-dev-qa-db-fra.com

Catch une exception levée par une méthode void async

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.

251
TimothyP

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.

233
Stuart

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
    }
}
70
Rob Church

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:

  • Créer 4 tâches
  • Chaque tâche va incrémenter de manière asynchrone un nombre et renvoyer le nombre incrémenté
  • Lorsque le résultat asynchrone est arrivé, il est tracé.

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.

17
Alois Kraus

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();
}
4

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 */     
        }
    }
4
rohanjansen

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 ...

1