Złap wyjątek zgłoszony przez asynchroniczną metodę void

283

Czy przy użyciu asynchronicznego protokołu CTP firmy Microsoft dla platformy .NET można wychwycić wyjątek zgłoszony przez metodę asynchroniczną w metodzie wywołującej?

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

Zasadniczo chcę, aby wyjątek od kodu asynchronicznego pojawił się w moim kodzie wywołującym, jeśli jest to w ogóle możliwe.

TimothyP
źródło
22
W przypadku, gdy ktoś natknie się na to w przyszłości, artykuł na temat najlepszych praktyk Async / Await ... zawiera dobre wyjaśnienie tego na „Rysunku 2 wyjątków od metody pustki Async nie można złapać”. „ Gdy wyjątek zostanie wyrzucony z metody asynchronicznej zadania lub metody asynchronicznej zadania <T>, ten wyjątek jest przechwytywany i umieszczany na obiekcie zadania. W przypadku metod async void nie ma obiektu zadania, a wyjątki są wyrzucane z metody asynchronicznej nieważności zostanie podniesiony bezpośrednio w SynchronizationContext, który był aktywny, gdy rozpoczęła się metoda asynchronicznej nieważności.
Pan Moose
Możesz użyć tego podejścia lub tego
Tselofan,

Odpowiedzi:

263

Jest to trochę dziwne do przeczytania, ale tak, wyjątek spowoduje wyświetlenie kodu wywołującego - ale tylko wtedy, gdy ty awaitlub Wait()połączenie zFoo .

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

Metody asynchroniczne void mają różne semantyki obsługi błędów. Gdy wyjątek zostanie wyrzucony z asynchronicznego zadania lub asynchronicznej metody zadania, wyjątek ten jest przechwytywany i umieszczany na obiekcie Task. W przypadku metod async void nie ma obiektu Task, więc wszelkie wyjątki wyrzucone z metody async void będą zgłaszane bezpośrednio w SynchronizationContext, który był aktywny podczas uruchamiania metody async void. - https://msdn.microsoft.com/en-us/magazine/jj991977.aspx

Zauważ, że użycie Wait () może powodować blokowanie twojej aplikacji, jeśli .Net zdecyduje się wykonać twoją metodę synchronicznie.

To wyjaśnienie http://www.interact-sw.co.uk/iangblog/2010/11/01/csharp5-async-exceptions jest całkiem dobre - omawia kroki kompilatora, aby osiągnąć tę magię.

Stuart
źródło
3
Mam na myśli to, że czytanie jest proste - podczas gdy wiem, że to, co się dzieje, jest naprawdę skomplikowane - więc mój mózg mówi mi, żebym nie wierzył własnym oczom ...
Stuart
8
Myślę, że metoda Foo () powinna być oznaczona jako Task zamiast void.
Sornii
4
Jestem prawie pewien, że spowoduje to wyjątek AggregateException. W związku z tym blok catch przedstawiony w tej odpowiedzi nie przechwytuje wyjątku.
xanadont
2
„ale tylko wtedy, gdy czekasz lub czekasz () na połączenie z Foo”. Jak możesz awaitzadzwonić do Foo, kiedy Foo powróci nieważne? async void Foo(). Type void is not awaitable?
rism
3
Nie możesz czekać na nieważną metodę, prawda?
Hitesh P
74

Powodem, dla którego wyjątek nie został wyłapany, jest to, że metoda Foo () ma typ void return, a więc po wywołaniu funkcji wyczekiwania po prostu zwraca. Ponieważ DoFoo () nie oczekuje na zakończenie Foo, nie można użyć procedury obsługi wyjątków.

Otwiera to prostsze rozwiązanie, czy można zmienić podpisów metoda - zmieniać Foo()tak, że zwraca typ Task, a następnie DoFoo()można await Foo(), jak w tym kodzie:

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
źródło
19
To naprawdę może cię podkraść i powinien zostać ostrzeżony przez kompilator.
GGleGrand,
19

Twój kod nie działa tak, jak myślisz. Metody asynchroniczne powracają natychmiast po tym, jak metoda zacznie czekać na wynik asynchroniczny. Wgląd w śledzenie w celu sprawdzenia, jak działa kod, jest wnikliwy.

Poniższy kod wykonuje następujące czynności:

  • Utwórz 4 zadania
  • Każde zadanie asynchronicznie zwiększy liczbę i zwróci liczbę zwiększoną
  • Po otrzymaniu wyniku asynchronicznego jest on śledzony.

 

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

Kiedy obserwujesz ślady

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   

Zauważysz, że metoda Run kończy się na wątku 2820, podczas gdy tylko jeden wątek potomny został zakończony (2756). Jeśli umieścisz metodę try / catch w metodzie oczekującej, możesz „złapać” wyjątek w zwykły sposób, chociaż kod jest wykonywany w innym wątku, gdy zadanie obliczeniowe jest zakończone, a połączenie jest wykonywane.

Metoda obliczania automatycznie śledzi zgłoszony wyjątek, ponieważ użyłem ApiChange.Api.dll z narzędzia ApiChange . Śledzenie i odbłyśnik bardzo pomaga zrozumieć, co się dzieje. Aby pozbyć się wątków, możesz utworzyć własne wersje GetAwaiter BeginAwait i EndAwait i nie zawijać zadania, ale np. Lazy i śledzić własne metody rozszerzenia. Wtedy znacznie lepiej zrozumiesz, co robi kompilator i co robi licencja TPL.

Teraz widzisz, że nie ma sposobu na odzyskanie wyjątku, ponieważ nie ma żadnej ramki stosu dla żadnego wyjątku do propagacji. Twój kod może robić coś zupełnie innego po zainicjowaniu operacji asynchronicznych. Może wywoływać Thread.Sleep lub nawet zakończyć. Tak długo, jak pozostanie jeden wątek pierwszego planu, aplikacja będzie z przyjemnością kontynuować wykonywanie zadań asynchronicznych.


Możesz obsłużyć wyjątek w metodzie asynchronicznej po zakończeniu operacji asynchronicznej i wywołaniu zwrotnym w wątku interfejsu użytkownika. Zalecanym sposobem jest skorzystanie z TaskScheduler.FromSynchronizationContext . Działa to tylko wtedy, gdy masz wątek interfejsu użytkownika i nie jest zbyt zajęty innymi rzeczami.

Alois Kraus
źródło
5

Wyjątek można przechwycić w funkcji asynchronicznej.

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
źródło
2
Hej, wiem, ale naprawdę potrzebuję tych informacji w DoFoo, aby wyświetlić te informacje w interfejsie użytkownika. W takim przypadku ważne jest, aby interfejs użytkownika wyświetlał wyjątek, ponieważ nie jest to narzędzie użytkownika końcowego, ale narzędzie do debugowania protokołu komunikacyjnego
TimothyP
W takim przypadku oddzwanianie ma wiele sensu. (Stare dobre asynchroniczne delegacje)
Sanjeevakumar Hiremath
@Tim: Uwzględnij wszelkie potrzebne informacje w zgłoszonym wyjątku?
Eric J.
5

Ważne jest również, aby pamiętać, że utracisz chronologiczny ślad stosu wyjątku, jeśli masz typ zwrotu void dla metody asynchronicznej. Polecam zwrócenie zadania w następujący sposób. Sprawi, że debugowanie będzie o wiele łatwiejsze.

public async Task DoFoo()
    {
        try
        {
            return await Foo();
        }
        catch (ProtocolException ex)
        {
            /* Exception with chronological stack trace */     
        }
    }
rohanjansen
źródło
Spowoduje to problem z tym, że nie wszystkie ścieżki zwracają wartość, ponieważ jeśli istnieje wyjątek, żadna wartość nie jest zwracana, podczas próby występuje. Jeśli nie masz returninstrukcji, ten kod działa jednak, ponieważ Task„niejawnie” jest zwracany za pomocą async / await.
Matias Grioni
2

Ten blog starannie wyjaśnia Twój problem Async Best Practices .

Istotą tego jest to, że nie powinieneś używać void jako zwrotu dla metody asynchronicznej, chyba że jest to asynchroniczna procedura obsługi zdarzeń, jest to zła praktyka, ponieważ nie pozwala na wychwytywanie wyjątków ;-).

Najlepszą praktyką byłoby zmienić typ zwrotu na Zadanie. Spróbuj także kodować asynchronicznie przez całą drogę, wywoływać każde wywołanie metody asynchronicznej i być wywoływanym z metod asynchronicznych. Z wyjątkiem metody Main w konsoli, która nie może być asynchroniczna (przed C # 7.1).

Jeśli zignorujesz tę najlepszą praktykę, wpadniesz w impas w aplikacjach GUI i ASP.NET. Zakleszczenie występuje, ponieważ aplikacje te działają w kontekście, który pozwala tylko na jeden wątek i nie zrzeknie się go w wątku asynchronicznym. Oznacza to, że GUI czeka synchronicznie na zwrot, podczas gdy metoda asynchroniczna czeka na kontekst: zakleszczenie.

To zachowanie nie występuje w aplikacji konsoli, ponieważ działa w kontekście z pulą wątków. Metoda asynchroniczna powróci w innym wątku, który zostanie zaplanowany. Dlatego aplikacja konsoli testowej będzie działać, ale te same połączenia będą działać w impasie w innych aplikacjach ...

Stephan Ghequiere
źródło
1
„Z wyjątkiem metody Main w konsoli, której nie można asynchronizować.” Od wersji C # 7.1 Main może być teraz linkiem do
Adam