Cattura un'eccezione lanciata con un metodo asincrero vuoto
-
28-10-2019 - |
Domanda
Utilizzando il CTP Async di Microsoft per .NET, è possibile catturare un'eccezione lanciata con un metodo Async 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, fondamentalmente, voglio che l'eccezione del codice Async si completa nel mio codice di chiamata, se ciò è anche possibile.
Soluzione
È un po 'strano da leggere, ma sì, l'eccezione bolle fino al codice chiamante - ma solo se tu await
o Wait()
la chiamata a Foo
.
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 vuoti asincroni hanno una semantica di gestione degli errori diversi. Quando un'eccezione viene lanciata da un'attività asincrona o metodo dell'attività asincrona, tale eccezione viene acquisita e posizionata sull'oggetto attività. Con i metodi del vuoto asincrone, non esiste un oggetto di attività, quindi eventuali eccezioni gettate fuori da un metodo asincrero verranno sollevate direttamente sul Context di sincronizzazione che era attivo quando è iniziato il metodo del vuoto asincrero. - 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 buono: discute i passaggi che il compilatore intraprende per raggiungere questa magia.
Altri suggerimenti
Il motivo per cui l'eccezione non è catturata è perché il metodo Foo () ha un tipo di ritorno vuoto e quindi quando viene chiamato, restituisce semplicemente. Poiché Dofoo () non attende il completamento di FOO, il gestore delle eccezioni non può essere utilizzato.
Questo apre una soluzione più semplice se è possibile modificare le firme del metodo - altera Foo()
in modo che restituisca il tipo Task
poi DoFoo()
Potere 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 che il metodo inizia ad aspettare il risultato asincrone. È perspicace usare la traccia per indagare su come si sta effettivamente comportando.
Il codice seguente fa quanto segue:
- Crea 4 compiti
- Ogni attività aumenterà in modo asincrono un numero e restituirà il numero incrementato
- Quando è arrivato il risultato asincrone, è rintracciato.
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 di corsa si completa sul thread 2820 mentre è terminato un solo thread figlio (2756). Se metti una prova/cattura intorno al metodo di attesa, 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 contiura viene eseguita.
Il metodo di calcolo traccia automaticamente l'eccezione lanciata perché ho usato apichange.api.dll dal Apichange attrezzo. La traccia e il riflettore aiutano molto a capire cosa sta succedendo. Per sbarazzarti del threading puoi creare le tue versioni di getwaiter Startawait e E di inaugurazione e avvolgere non un compito ma ad esempio una pigra e traccia all'interno dei tuoi metodi di estensione. Quindi avrai molto meglio capire cosa fa il compilatore e cosa fa il TPL.
Ora vedi che non c'è modo di provare/recuperare la tua eccezione poiché non è rimasto un telaio di stack per non eccezione da cui propagarsi. Il tuo codice potrebbe fare qualcosa di totalmente diverso dopo aver avviato le operazioni asincroni. Potrebbe chiamare thread.sleep o addirittura terminare. Finché c'è un thread di primo piano lasciato, la tua applicazione continuerà felicemente a eseguire compiti asincroni.
È possibile gestire l'eccezione all'interno del metodo Async dopo che l'operazione asincrona è finita e richiamata 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.
L'eccezione può essere catturata nella funzione Async.
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();
}
È anche importante notare che perderai la traccia cronologica dello stack dell'eccezione se si dispone di un tipo di ritorno a vuoto su un metodo asincrone. Consiglierei di restituire l'attività come segue. Andare a rendere il debug molto più semplice.
public async Task DoFoo()
{
try
{
return await Foo();
}
catch (ProtocolException ex)
{
/* Exception with chronological stack trace */
}
}
Questo blog spiega il tuo problema in modo ordinato Le migliori pratiche asincroni.
L'essenza di essa non dovresti usare il vuoto come ritorno per un metodo asincrone, a meno che non sia un gestore di eventi asincroni, questa è una cattiva pratica perché non consente di catturare le eccezioni ;-).
Le migliori pratiche sarebbero quella di modificare il tipo di ritorno in attività. Inoltre, prova a codificare Async fino in fondo, fai chiamare ogni metodo Async e essere chiamato dai metodi Async. Ad eccezione di un metodo principale in una console, che non può essere asincrone (prima di C# 7.1).
Ti imbatterai in deadlock con applicazioni GUI e ASP.NET se ignori questa migliore pratica. Il deadlock si verifica perché queste applicazioni vengono eseguite su un contesto che consente solo un thread e non lo rinuncia al thread asincrone. Ciò significa che la GUI attende in modo sincrono per un ritorno, mentre il metodo Async attende il contesto: Deadlock.
Questo comportamento non avverrà in un'applicazione della console, perché funziona nel contesto con un pool di thread. Il metodo Async tornerà su un altro thread che sarà programmato. Questo è il motivo per cui un'app di console di test funzionerà, ma le stesse chiamate saranno deadlock in altre applicazioni ...