Cattura un’eccezione generata da un metodo asincrono

Utilizzando il CTP asincrono di Microsoft per .NET, è ansible rilevare un’eccezione generata da un metodo asincrono nel metodo di chiamata?

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 in pratica voglio che l’eccezione del codice asincrono diventi il ​​mio codice chiamante, se è addirittura ansible.

È un po ‘strano da leggere, ma sì, l’eccezione salirà al codice chiamante, ma solo se await o Wait() la chiamata a 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. */ } } 

I metodi di vuoto asincrono hanno una semantica di gestione degli errori diversa. Quando un’eccezione viene espulsa da un task asincrono o da un task async, quell’eccezione viene catturata e posizionata sull’object Task. Con i metodi void asincroni, non esiste alcun object Task, quindi tutte le eccezioni eliminate da un metodo asincrono void verranno generate direttamente sul SynchronizationContext che era attivo all’avvio del metodo void async. – https://msdn.microsoft.com/en-us/magazine/jj991977.aspx

Si noti che l’uso 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 – tratta i passaggi che il compilatore impiega per raggiungere questa magia.

Il motivo per cui l’eccezione non viene rilevata è perché il metodo Foo () ha un tipo di ritorno vuoto e quindi quando viene richiamato l’attesa, viene semplicemente restituito. Poiché DoFoo () non è in attesa del completamento di Foo, non è ansible utilizzare il gestore delle eccezioni.

Ciò apre una soluzione più semplice se è ansible modificare le firme dei metodi – alter Foo() modo che restituisca type Task e quindi DoFoo() possa 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 } } 

Il tuo codice non fa quello che potresti pensare che faccia. I metodi asincroni tornano immediatamente dopo l’inizio del metodo in attesa del risultato asincrono. È intuitivo utilizzare la traccia per indagare come si comporta effettivamente il codice.

Il codice seguente fa quanto segue:

  • Crea 4 compiti
  • Ogni attività incrementerà in modo asincrono un numero e restituirà 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 Calculate(int i) { var t = new Task(() => { 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 

Si noterà che il metodo Run viene completato sulla discussione 2820 mentre è terminato un solo thread figlio (2756). Se ci provi / ti aggiri attorno al tuo metodo await puoi “catturare” l’eccezione nel solito modo sebbene il tuo codice sia eseguito su un altro thread quando l’attività di calcolo è terminata e la tua esecuzione è eseguita.

Il metodo di calcolo traccia automaticamente l’eccezione generata perché ho utilizzato ApiChange.Api.dll dallo strumento ApiChange . Tracing and Reflector aiuta molto a capire cosa sta succedendo. Per sbarazzarsi del threading è ansible creare le proprie versioni di GetAwaiter BeginAwait e EndAwait e non eseguire il wrapping di un’attività, ad esempio un Lazy e tracciare i propri metodi di estensione. In questo modo capirai meglio cosa fa il compilatore e cosa fa la TPL.

Ora vedete che non c’è modo di provare e riprendere la vostra eccezione poiché non vi è più alcun stack di frame da cui propagarsi un’eccezione. Il tuo codice potrebbe fare qualcosa di completamente diverso dopo aver avviato le operazioni asincrone. Potrebbe chiamare Thread.Sleep o addirittura terminare. Finché c’è un solo thread in primo piano la tua applicazione continuerà felicemente ad eseguire task asincroni.


È ansible gestire l’eccezione all’interno del metodo asincrono dopo che l’operazione asincrona è terminata 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 occupato con altre cose.

È inoltre importante notare che si perderà la traccia cronologica dello stack dell’eccezione se si dispone di un tipo di reso vuoto su un metodo asincrono. Vorrei raccomandare di tornare a Task come segue. Fare il debug molto più facilmente.

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

L’eccezione può essere catturata 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(); } 

Questo blog spiega con precisione il tuo problema Async Best Practices .

In sostanza, non dovresti usare void come return per un metodo asincrono, a meno che non si tratti di un gestore di eventi asincrono, questa è una ctriggers pratica perché non consente di catturare le eccezioni ;-).

La migliore pratica sarebbe quella di cambiare il tipo di ritorno in Attività. Inoltre, prova a codificare async fino in fondo, fai ogni chiamata al metodo asincrono e chiama dai metodi asincroni. Tranne un metodo Main in una console, che non può essere asincrono (prima di C # 7.1).

Si verificheranno deadlock con le applicazioni GUI e ASP.NET se si ignora 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 verificherà in un’applicazione console, poiché viene eseguito sul contesto con un pool di thread. Il metodo async tornerà su un altro thread che verrà pianificato. Ecco perché un’app per console di test funzionerà, ma le stesse chiamate si bloccano in altre applicazioni …