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.

211
demandé sur InteXX 2011-03-21 23:34:38

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.

193
répondu Stuart 2018-04-05 04:49:50

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
    }
}
63
répondu Rob Church 2013-05-22 11:04:10

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.

16
répondu Alois Kraus 2018-07-21 17:47:16

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 */     
        }
    }
4
répondu rohanjansen 2014-07-04 12:17:12

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();
}
3
répondu Sanjeevakumar Hiremath 2011-03-21 20:50:02

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

1
répondu Stephan Ghequiere 2018-07-09 11:03:06