Cattura un'eccezione generata da un metodo void asincrono


283

Utilizzando il CTP asincrono di Microsoft per .NET, è possibile rilevare un'eccezione generata da un metodo asincrono nel metodo chiamante?

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

Quindi, fondamentalmente, voglio che l'eccezione dal codice asincrono si riversi nel mio codice di chiamata, se possibile.



22
Nel caso in cui qualcuno dovesse inciampare su questo in futuro, l'articolo Async / Await Best Practices ... ha una buona spiegazione in "Figura 2 Eccezioni da un metodo Async Void non può essere catturato con Catch". " Quando viene generata un'eccezione da un'attività asincrona o dal metodo dell'attività <T> asincrona, tale eccezione viene acquisita e posizionata sull'oggetto Task. Con i metodi void asincroni, non esiste alcun oggetto Task, nessuna eccezione generata da un metodo void asincrono verrà generato direttamente sul SynchronizationContext che era attivo quando è iniziato il metodo del vuoto asincrono. "
Moose

Risposte:


263

È un po 'strano da leggere, ma sì, l'eccezione si riverserà nel codice chiamante, ma solo se tu awaito Wait()la chiamata aFoo .

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

I metodi void asincroni hanno una semantica di gestione degli errori diversa. Quando viene generata un'eccezione da un'attività asincrona o da un metodo dell'attività asincrona, tale eccezione viene acquisita e posizionata sull'oggetto Task. Con i metodi void asincroni, non esiste alcun oggetto Task, quindi eventuali eccezioni generate da un metodo void asincrono verranno sollevate direttamente su SynchronizationContext che era attivo all'avvio del metodo void asincrono. - https://msdn.microsoft.com/en-us/magazine/jj991977.aspx

Si noti che l'utilizzo di Wait () può causare il blocco dell'applicazione, se .Net decide di eseguire il metodo in modo sincrono.

Questa spiegazione http://www.interact-sw.co.uk/iangblog/2010/11/01/csharp5-async-exceptions è abbastanza buona: discute i passi che il compilatore prende per raggiungere questa magia.


3
Intendo dire che è semplice leggere - mentre so che quello che sta realmente accadendo è davvero complicato - quindi il mio cervello mi sta dicendo di non credere ai miei occhi ...
Stuart

8
Penso che il metodo Foo () dovrebbe essere contrassegnato come Task anziché vuoto.
Sornii,

4
Sono abbastanza sicuro che questo produrrà una AggregateException. Come tale, il blocco catch come appare in questa risposta non catturerà l'eccezione.
xanadont,

2
"ma solo se aspetti o attendi () la chiamata a Foo" Come puoi awaitannullare la chiamata a Foo, quando Foo sta tornando? async void Foo(). Type void is not awaitable?
ris

3
Non puoi aspettare il metodo vuoto, vero?
Hitesh P

74

Il motivo per cui l'eccezione non viene rilevata è perché il metodo Foo () ha un tipo di ritorno vuoto e quindi quando viene chiamato waitit, ritorna semplicemente. Poiché DoFoo () non è in attesa del completamento di Foo, il gestore delle eccezioni non può essere utilizzato.

Questo apre una soluzione più semplice, se è possibile modificare le firme dei metodi - altera Foo()in modo che restituisca il tipo Taske quindi DoFoo()può await Foo(), come in questo codice:

public async Task Foo() {
    var x = await DoSomethingThatThrows();
}

public async void DoFoo() {
    try {
        await Foo();
    } catch (ProtocolException ex) {
        // This will catch exceptions from DoSomethingThatThrows
    }
}

19
Questo può davvero nasconderti e dovrebbe essere avvertito dal compilatore.
GGleGrand,

19

Il tuo codice non fa quello che potresti pensare che faccia. I metodi asincroni ritornano immediatamente dopo che il metodo inizia ad attendere il risultato asincrono. È perspicace usare la traccia per studiare come si sta comportando il codice.

Il codice seguente effettua le seguenti operazioni:

  • Crea 4 attività
  • Ogni attività incrementa in modo asincrono un numero e restituisce il numero incrementato
  • Quando il risultato asincrono è arrivato, viene tracciato.

 

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

Quando osservi le tracce

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   

Noterai che il metodo Run viene completato sul thread 2820 mentre è terminato un solo thread figlio (2756). Se metti un tentativo / catch intorno al tuo metodo waitit puoi "catturare" l'eccezione nel solito modo sebbene il tuo codice sia eseguito su un altro thread quando l'attività di calcolo è terminata e il tuo conteggio viene eseguito.

Il metodo di calcolo traccia automaticamente l'eccezione generata perché ho usato ApiChange.Api.dll da ApiChange strumento . Tracing and Reflector aiuta molto a capire cosa sta succedendo. Per sbarazzarsi del threading è possibile creare le proprie versioni di GetAwaiter BeginAwait ed EndAwait e concludere non un'attività ma ad esempio un Pigro e tracciare all'interno dei propri metodi di estensione. Quindi otterrai una comprensione molto migliore di ciò che il compilatore e cosa fa il TPL.

Ora vedi che non c'è modo di entrare in un tentativo / recuperare la tua eccezione poiché non è rimasto alcun frame di stack per la propagazione di qualsiasi eccezione. Il codice potrebbe fare qualcosa di completamente diverso dopo aver avviato le operazioni asincrone. Potrebbe chiamare Thread.Sleep o addirittura terminare. Finché rimane un thread in primo piano, l'applicazione continuerà felicemente a eseguire attività asincrone.


È possibile gestire l'eccezione all'interno del metodo asincrono al termine dell'operazione asincrona e richiamare nel thread dell'interfaccia utente. Il modo consigliato per farlo è con TaskScheduler.FromSynchronizationContext . Funziona solo se hai un thread dell'interfaccia utente e non è molto impegnato con altre cose.


5

L'eccezione può essere rilevata nella funzione asincrona.

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

2
Ehi, lo so ma ho davvero bisogno di tali informazioni in DoFoo in modo da poter visualizzare le informazioni nell'interfaccia utente. In questo caso è importante che l'interfaccia utente visualizzi l'eccezione in quanto non è uno strumento per l'utente finale ma uno strumento per il debug di un protocollo di comunicazione
TimothyP

In tal caso, i callback hanno molto senso. (Buoni vecchi delegati asincroni)
Sanjeevakumar Hiremath

@Tim: includere le informazioni necessarie nell'eccezione generata?
Eric J.

5

È anche importante notare che perderai la traccia dello stack cronologico dell'eccezione se hai un tipo di ritorno vuoto su un metodo asincrono. Consiglierei di restituire Task come segue. Rende il debugging molto più semplice.

public async Task DoFoo()
    {
        try
        {
            return await Foo();
        }
        catch (ProtocolException ex)
        {
            /* Exception with chronological stack trace */     
        }
    }

Ciò causerà un problema con non tutti i percorsi che restituiscono un valore, poiché se esiste un'eccezione non viene restituito alcun valore, mentre nel tentativo c'è. Se non si dispone di alcuna returnistruzione, questo codice funziona comunque, poiché Taskviene restituito "implicitamente" utilizzando async / await.
Matias Grioni,

2

Questo blog spiega chiaramente il tuo problema con le migliori pratiche asincrone .

Il fatto è che non dovresti usare void come ritorno per un metodo asincrono, a meno che non sia un gestore di eventi asincrono, questa è una cattiva pratica perché non consente di catturare le eccezioni ;-).

La migliore pratica sarebbe quella di cambiare il tipo di ritorno in Task. Inoltre, prova a codificare in modo asincrono fino in fondo, effettua tutte le chiamate al metodo asincrono ed essere chiamato da metodi asincroni. Ad eccezione di un metodo Main in una console, che non può essere asincrono (prima di C # 7.1).

Incontrerai deadlock con applicazioni GUI e ASP.NET se ignori questa best practice. Il deadlock si verifica perché queste applicazioni vengono eseguite in un contesto che consente solo un thread e non lo abbandonerà al thread asincrono. Ciò significa che la GUI attende in modo sincrono un ritorno, mentre il metodo asincrono attende il contesto: deadlock.

Questo comportamento non si verifica in un'applicazione console, poiché viene eseguito in un contesto con un pool di thread. Il metodo asincrono tornerà su un altro thread che verrà programmato. Ecco perché un'app della console di test funzionerà, ma le stesse chiamate si bloccheranno in altre applicazioni ...


1
"Ad eccezione di un metodo Main in una console, che non può essere asincrono." Da C # 7.1, Main ora può essere un collegamento al
Adam,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.