Una delle principali differenze è nella propagazione delle eccezioni. Un'eccezione, generata all'interno di un async Taskmetodo, viene memorizzato nella restituito Taskoggetto e rimane inattivo fino a quando il compito viene osservata tramite await task, task.Wait(), task.Resulto task.GetAwaiter().GetResult(). Viene propagato in questo modo anche se lanciato dalla parte sincrona del asyncmetodo.
Considera il codice seguente, dove OneTestAsynce AnotherTestAsynccomportati in modo abbastanza diverso:
static async Task OneTestAsync(int n)
{
await Task.Delay(n);
}
static Task AnotherTestAsync(int n)
{
return Task.Delay(n);
}
static void DoTestAsync(Func<int, Task> whatTest, int n)
{
Task task = null;
try
{
task = whatTest(n);
Console.Write("Press enter to continue");
Console.ReadLine();
task.Wait();
}
catch (Exception ex)
{
Console.Write("Error: " + ex.Message);
}
}
Se chiamo DoTestAsync(OneTestAsync, -2), produce il seguente output:
Premi Invio per continuare
Errore: si sono verificati uno o più errori. Attiva Task.Delay
Errore: 2 °
Nota, ho dovuto premere Enterper vederlo.
Ora, se chiamo DoTestAsync(AnotherTestAsync, -2), il flusso di lavoro del codice all'interno DoTestAsyncè abbastanza diverso, così come l'output. Questa volta non mi è stato chiesto di premere Enter:
Errore: il valore deve essere -1 (che indica un timeout infinito), 0 o un numero intero positivo.
Nome parametro: millisecondsDelayError: 1st
In entrambi i casi Task.Delay(-2)lancia all'inizio, convalidando i suoi parametri. Questo potrebbe essere uno scenario inventato, ma in teoria Task.Delay(1000)potrebbe anche generare, ad esempio, quando l'API del timer di sistema sottostante fallisce.
In una nota a margine, la logica di propagazione dell'errore è ancora diversa per i async voidmetodi (rispetto ai async Taskmetodi). Un'eccezione sollevata all'interno di un async voidmetodo verrà immediatamente rilanciata nel contesto di sincronizzazione del thread corrente (tramite SynchronizationContext.Post), se il thread corrente ne ha uno ( SynchronizationContext.Current != null). Altrimenti, verrà rilanciata tramite ThreadPool.QueueUserWorkItem). Il chiamante non ha la possibilità di gestire questa eccezione sullo stesso stack frame.
Ho pubblicato alcuni dettagli in più sul comportamento di gestione delle eccezioni TPL qui e qui .
D : È possibile imitare il comportamento di propagazione delle eccezioni dei asyncmetodi per metodi non asincroni Task, in modo che questi ultimi non vengano lanciati sullo stesso stack frame?
A : Se davvero necessario, allora sì, c'è un trucco per questo:
async Task<int> MethodAsync(int arg)
{
if (arg < 0)
throw new ArgumentException("arg");
return 42 + arg;
}
Task<int> MethodAsync(int arg)
{
var task = new Task<int>(() =>
{
if (arg < 0)
throw new ArgumentException("arg");
return 42 + arg;
});
task.RunSynchronously(TaskScheduler.Default);
return task;
}
Nota tuttavia, in determinate condizioni (come quando è troppo in profondità nello stack), RunSynchronouslypotrebbe ancora essere eseguito in modo asincrono.
Un'altra differenza notevole è che
la versione async/ awaitè più incline al dead-lock su un contesto di sincronizzazione non predefinito . Ad esempio, quanto segue sarà dead-lock in un'applicazione WinForms o WPF:
static async Task TestAsync()
{
await Task.Delay(1000);
}
void Form_Load(object sender, EventArgs e)
{
TestAsync().Wait();
}
Cambialo in una versione non asincrona e non si bloccherà:
Task TestAsync()
{
return Task.Delay(1000);
}
La natura del dead-lock è ben spiegata da Stephen Cleary nel suo blog .
await/asyncaffatto :)