Fangen Sie eine Ausnahme ab, die von einer asynchronen Void-Methode ausgelöst wird

282

Ist es mit dem asynchronen CTP von Microsoft für .NET möglich, eine Ausnahme abzufangen, die von einer asynchronen Methode in der aufrufenden Methode ausgelöst wird?

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. */
    }
}

Grundsätzlich möchte ich, dass die Ausnahme vom asynchronen Code in meinen aufrufenden Code übergeht, wenn dies überhaupt möglich ist.

TimothyP
quelle
22
Für den Fall, dass jemand in Zukunft darauf stößt, finden Sie im Artikel Async / Await Best Practices ... eine gute Erklärung in "Abbildung 2 Ausnahmen von einer Async Void-Methode können nicht mit dem Fang erfasst werden". " Wenn eine Ausnahme aus einer asynchronen Task- oder asynchronen Task <T> -Methode ausgelöst wird, wird diese Ausnahme erfasst und auf dem Task-Objekt platziert. Bei asynchronen void-Methoden gibt es kein Task-Objekt, keine Ausnahmen, die aus einer asynchronen void-Methode ausgelöst wurden wird direkt im SynchronizationContext ausgelöst, der aktiv war, als die asynchrone Void-Methode gestartet wurde. "
Mr Moose
Sie können diesen Ansatz oder diesen verwenden
Tselofan

Antworten:

262

Es ist etwas seltsam zu lesen, aber ja, die Ausnahme sprudelt bis zum aufrufenden Code - aber nur, wenn Sie awaitoder Wait()der Anruf beiFoo .

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. */
    }
} 

Asynchrone Void-Methoden haben unterschiedliche Semantik zur Fehlerbehandlung. Wenn eine Ausnahme aus einer asynchronen Task oder einer asynchronen Task-Methode ausgelöst wird, wird diese Ausnahme erfasst und im Task-Objekt platziert. Bei asynchronen void-Methoden gibt es kein Task-Objekt. Daher werden alle Ausnahmen, die aus einer async void-Methode ausgelöst werden, direkt im SynchronizationContext ausgelöst, der beim Start der async void-Methode aktiv war. - https://msdn.microsoft.com/en-us/magazine/jj991977.aspx

Beachten Sie, dass die Verwendung von Wait () dazu führen kann, dass Ihre Anwendung blockiert wird, wenn .Net beschließt, Ihre Methode synchron auszuführen.

Diese Erklärung http://www.interact-sw.co.uk/iangblog/2010/11/01/csharp5-async-exceptions ist ziemlich gut - sie beschreibt die Schritte, die der Compiler unternimmt, um diese Magie zu erreichen.

Stuart
quelle
3
Ich meine eigentlich, es ist einfach zu lesen - während ich weiß, was wirklich los ist, ist wirklich kompliziert -, also sagt mir mein Gehirn, ich soll meinen Augen nicht trauen ...
Stuart
8
Ich denke, die Foo () -Methode sollte als Task anstatt als void markiert werden.
Sornii
4
Ich bin mir ziemlich sicher, dass dies eine AggregateException erzeugt. Daher fängt der Fangblock, wie er in dieser Antwort angezeigt wird, die Ausnahme nicht ab.
Xanadont
2
"aber nur, wenn Sie auf den Anruf bei Foo warten oder warten ()" Wie können Sie awaitden Anruf bei Foo tätigen, wenn Foo ungültig zurückkehrt? async void Foo(). Type void is not awaitable?
Rism
3
Kann die Methode der Leere nicht abwarten, oder?
Hitesh P
74

Der Grund, warum die Ausnahme nicht abgefangen wird, liegt darin, dass die Foo () -Methode einen ungültigen Rückgabetyp hat. Wenn also await aufgerufen wird, wird sie einfach zurückgegeben. Da DoFoo () nicht auf den Abschluss von Foo wartet, kann der Ausnahmebehandler nicht verwendet werden.

Es öffnet sich eine einfachere Lösung, wenn Sie die Methodensignaturen ändern - ändern , Foo()so dass es Typ zurückgibt Taskund dann DoFoo()kann await Foo(), wie in diesem 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
    }
}
Rob Church
quelle
19
Dies kann sich wirklich an Sie anschleichen und sollte vom Compiler gewarnt werden.
GGleGrand
19

Ihr Code macht nicht das, was Sie vielleicht denken. Asynchrone Methoden kehren sofort zurück, nachdem die Methode auf das asynchrone Ergebnis gewartet hat. Es ist aufschlussreich, die Ablaufverfolgung zu verwenden, um zu untersuchen, wie sich der Code tatsächlich verhält.

Der folgende Code bewirkt Folgendes:

  • Erstellen Sie 4 Aufgaben
  • Jede Aufgabe erhöht asynchron eine Nummer und gibt die inkrementierte Nummer zurück
  • Wenn das asynchrone Ergebnis eingetroffen ist, wird es verfolgt.

 

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;
}

Wenn Sie die Spuren beobachten

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   

Sie werden feststellen, dass die Run-Methode für Thread 2820 abgeschlossen ist, während nur ein untergeordneter Thread abgeschlossen ist (2756). Wenn Sie Ihre Wartemethode versuchen / fangen, können Sie die Ausnahme auf die übliche Weise "fangen", obwohl Ihr Code in einem anderen Thread ausgeführt wird, wenn die Berechnungsaufgabe abgeschlossen ist und Ihre Fortsetzung ausgeführt wird.

Die Berechnungsmethode verfolgt die ausgelöste Ausnahme automatisch, da ich die ApiChange.Api.dll aus dem verwendet habe ApiChange- Tool verwendet habe. Tracing and Reflector hilft sehr zu verstehen, was los ist. Um das Threading zu vermeiden, können Sie Ihre eigenen Versionen von GetAwaiter BeginAwait und EndAwait erstellen und keine Aufgabe, sondern z. B. Lazy and Trace in Ihre eigenen Erweiterungsmethoden einbinden. Dann werden Sie viel besser verstehen, was der Compiler und was die TPL tut.

Jetzt sehen Sie, dass es keine Möglichkeit gibt, Ihre Ausnahme erneut zu versuchen / abzufangen, da kein Stapelrahmen mehr vorhanden ist, von dem aus eine Ausnahme weitergegeben werden kann. Ihr Code macht möglicherweise etwas völlig anderes, nachdem Sie die asynchronen Vorgänge gestartet haben. Es könnte Thread.Sleep aufrufen oder sogar beenden. Solange noch ein Vordergrund-Thread vorhanden ist, führt Ihre Anwendung weiterhin asynchrone Aufgaben aus.


Sie können die Ausnahme innerhalb der asynchronen Methode behandeln, nachdem Ihre asynchrone Operation abgeschlossen wurde, und den UI-Thread zurückrufen. Die empfohlene Methode hierfür ist TaskScheduler.FromSynchronizationContext . Das funktioniert nur, wenn Sie einen UI-Thread haben und er nicht sehr beschäftigt mit anderen Dingen ist.

Alois Kraus
quelle
5

Die Ausnahme kann in der asynchronen Funktion abgefangen werden.

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();
}
Sanjeevakumar Hiremath
quelle
2
Hey, ich weiß, aber ich brauche diese Informationen wirklich in DoFoo, damit ich die Informationen in der Benutzeroberfläche anzeigen kann. In diesem Fall ist es wichtig, dass die Benutzeroberfläche die Ausnahme anzeigt, da es sich nicht um ein Endbenutzer-Tool handelt, sondern um ein Tool zum Debuggen eines Kommunikationsprotokolls
TimothyP
In diesem Fall sind Rückrufe sehr sinnvoll (gute alte asynchrone Delegierte)
Sanjeevakumar Hiremath
@ Tim: Fügen Sie alle benötigten Informationen in die ausgelöste Ausnahme ein?
Eric J.
5

Es ist auch wichtig zu beachten, dass Sie die chronologische Stapelverfolgung der Ausnahme verlieren, wenn Sie einen ungültigen Rückgabetyp für eine asynchrone Methode haben. Ich würde empfehlen, Task wie folgt zurückzugeben. Das Debuggen wird viel einfacher.

public async Task DoFoo()
    {
        try
        {
            return await Foo();
        }
        catch (ProtocolException ex)
        {
            /* Exception with chronological stack trace */     
        }
    }
Rohanjansen
quelle
Dies führt zu einem Problem, bei dem nicht alle Pfade einen Wert zurückgeben, da bei einer Ausnahme kein Wert zurückgegeben wird, während dies beim Versuch der Fall ist. Wenn Sie keine returnAnweisung haben, funktioniert dieser Code jedoch, da der Taskmit "implizit" zurückgegeben wird async / await.
Matias Grioni
2

Dieser Blog erklärt Ihr Problem genau . Async Best Practices .

Das Wesentliche dabei ist, dass Sie void nicht als Rückgabe für eine asynchrone Methode verwenden sollten, es sei denn, es handelt sich um einen asynchronen Ereignishandler. Dies ist eine schlechte Vorgehensweise, da keine Ausnahmen abgefangen werden können ;-).

Die beste Vorgehensweise wäre, den Rückgabetyp in Task zu ändern. Versuchen Sie außerdem, Async vollständig zu codieren, jeden asynchronen Methodenaufruf auszuführen und von asynchronen Methoden aufgerufen zu werden. Mit Ausnahme einer Main-Methode in einer Konsole, die nicht asynchron sein kann (vor C # 7.1).

Wenn Sie diese bewährte Methode ignorieren, treten bei GUI- und ASP.NET-Anwendungen Deadlocks auf. Der Deadlock tritt auf, weil diese Anwendungen in einem Kontext ausgeführt werden, der nur einen Thread zulässt und ihn nicht an den asynchronen Thread abgibt. Dies bedeutet, dass die GUI synchron auf eine Rückgabe wartet, während die asynchrone Methode auf den Kontext wartet: Deadlock.

Dieses Verhalten tritt in einer Konsolenanwendung nicht auf, da es im Kontext mit einem Thread-Pool ausgeführt wird. Die asynchrone Methode gibt einen anderen Thread zurück, der geplant wird. Aus diesem Grund funktioniert eine Testkonsolen-App, aber die gleichen Aufrufe werden in anderen Anwendungen blockiert ...

Stephan Ghequiere
quelle
1
"Mit Ausnahme einer Main-Methode in einer Konsole, die nicht asynchron sein kann." Da C # 7.1, Main kann nun ein asynchrones Methode Link
Adam