Nota
L'accesso a questa pagina richiede l'autorizzazione. È possibile provare ad accedere o modificare le directory.
L'accesso a questa pagina richiede l'autorizzazione. È possibile provare a modificare le directory.
Async/await semplifica la programmazione asincrona, ma alcuni errori vengono visualizzati ripetutamente. Questo articolo descrive i cinque bug più comuni nel codice asincrono e illustra come risolverli.
Il metodo asincrono viene eseguito in modo sincrono
L'aggiunta della parola chiave async a un metodo non comporta l'esecuzione del metodo in un thread in background. Indica al compilatore di consentire await all'interno del corpo del metodo e di eseguire il wrapping del valore restituito in un oggetto Task. Quando si richiama un metodo asincrono, viene eseguito in modo sincrono fino a raggiungere il primo await su un oggetto awaitable incompleto. Se il metodo non contiene espressioni await, o se ogni elemento awaitable che attende è già completo, il metodo viene completato interamente sul thread di chiamata:
public static class SyncExecutionExample
{
public static Task<int> ComputeAsync()
{
// No await in this method — it runs entirely synchronously.
return Task.FromResult(42);
}
}
Public Module SyncExecutionExample
Public Function ComputeAsync() As Task(Of Integer)
' No Await in this method — it runs entirely synchronously.
Return Task.FromResult(42)
End Function
End Module
In questo caso, il metodo restituisce immediatamente un task completato perché non genera mai. Il compilatore genera un avviso quando un metodo asincrono non dispone di await espressioni.
Se l'obiettivo è quello di eseguire l'offload di compiti vincolati alla CPU in un thread del pool di thread, usare Run anziché async.
public static class OffloadExample
{
public static int ComputeIntensive()
{
int sum = 0;
for (int i = 0; i < 1_000; i++)
sum += i;
return sum;
}
public static Task<int> ComputeOnThreadPoolAsync()
{
return Task.Run(() => ComputeIntensive());
}
}
Public Module OffloadExample
Public Function ComputeIntensive() As Integer
Dim sum As Integer = 0
For i As Integer = 0 To 999
sum += i
Next
Return sum
End Function
Public Function ComputeOnThreadPoolAsync() As Task(Of Integer)
Return Task.Run(Function() ComputeIntensive())
End Function
End Module
Per indicazioni su come utilizzare Task.Run, consultare Wrapper asincroni per i metodi sincroni.
Non è possibile attendere un metodo void asincrono
Quando si converte un metodo void sincrono in asincrono, modificare il tipo restituito in Task. Se si lascia il tipo restituito come void, il metodo diventa "async void", che non può essere atteso:
public static class AsyncVoidExample
{
// BAD: async void — can't be awaited.
public static async void DoWorkBadAsync()
{
await Task.Delay(100);
}
// GOOD: async Task — callers can await this.
public static async Task DoWorkGoodAsync()
{
await Task.Delay(100);
}
}
Public Module AsyncVoidExample
' BAD: Async Sub — can't be awaited.
Public Async Sub DoWorkBadAsync()
Await Task.Delay(100)
End Sub
' GOOD: Async Function returning Task — callers can await this.
Public Async Function DoWorkGoodAsync() As Task
Await Task.Delay(100)
End Function
End Module
I metodi void asincroni servono a uno scopo specifico: gestori eventi di primo livello nei framework dell'interfaccia utente. Al di fuori dei gestori di eventi, restituire sempre Task o Task<T> dai metodi asincroni. I metodi Async void presentano questi svantaggi:
- Le eccezioni non vengono rilevate. Le eccezioni generate in un metodo void asincrono vengono propagate all'oggetto attivo all'avvio SynchronizationContext del metodo. Il chiamante non riesce a intercettare queste eccezioni.
- I chiamanti non possono tenere traccia del completamento. Senza
Task, non esiste alcun meccanismo per sapere quand'è terminata l'operazione. - La verifica è difficile. Non è possibile attendere il metodo in un test per verificarne il comportamento.
Deadlock bloccando il codice asincrono
Questo bug è la causa più comune del codice asincrono che "non viene mai completato". Si verifica quando si blocca in modo sincrono (chiamare Wait, Task<TResult>.Result o GetAwaiter.GetResult) su un thread con un thread a singolo-thread SynchronizationContext.
Sequenza che causa un deadlock:
- Il codice nel thread dell'interfaccia utente (o nel thread di richiesta ASP.NET in ASP.NET meno recenti) chiama un metodo asincrono e blocca sull'attività restituita.
- Il metodo asincrono attende un'attività incompleta senza usare
ConfigureAwait(false). - Al termine dell'attività attesa, la continuazione tenta di effettuare un postback all'oggetto originale
SynchronizationContext. - Il thread del contesto è bloccato in attesa del completamento dell'attività, ovvero il deadlock.
public static class DeadlockExample
{
public static async Task<string> GetDataAsync()
{
// Without ConfigureAwait(false), this continuation
// posts back to the original SynchronizationContext.
await Task.Delay(100);
return "data";
}
public static void CallerThatDeadlocks()
{
// On a single-threaded SynchronizationContext (e.g. UI thread),
// the following line deadlocks because the continuation needs
// the same thread that .Result is blocking.
string result = GetDataAsync().Result;
}
}
Public Module DeadlockExample
Public Async Function GetDataAsync() As Task(Of String)
' Without ConfigureAwait(False), this continuation
' posts back to the original SynchronizationContext.
Await Task.Delay(100)
Return "data"
End Function
Public Sub CallerThatDeadlocks()
' On a single-threaded SynchronizationContext (e.g. UI thread),
' the following line deadlocks because the continuation needs
' the same thread that .Result is blocking.
Dim result As String = GetDataAsync().Result
End Sub
End Module
Come evitare stalli
Usare una o più di queste strategie:
Non bloccare. Usare
awaitinvece di.Resulto.Wait():public static class DeadlockFix1 { public static async Task CallerFixedAsync() { // Use await instead of .Result string result = await DeadlockExample.GetDataAsync(); Console.WriteLine(result); } }Public Module DeadlockFix1 Public Async Function CallerFixedAsync() As Task ' Use Await instead of .Result Dim result As String = Await DeadlockExample.GetDataAsync() Console.WriteLine(result) End Function End ModuleUsare
ConfigureAwait(false)nel codice della libreria. Quando il metodo di libreria non deve riprendere nel contesto del chiamante, specificareConfigureAwait(false)in ogniawait:public static class DeadlockFix2 { public static async Task<string> GetDataSafeAsync() { await Task.Delay(100).ConfigureAwait(false); return "data"; } }Public Module DeadlockFix2 Public Async Function GetDataSafeAsync() As Task(Of String) Await Task.Delay(100).ConfigureAwait(False) Return "data" End Function End ModuleL'uso di
ConfigureAwait(false)indica al runtime di non effettuare il marshalling della continuazione all'originaleSynchronizationContext. Questo approccio protegge i chiamanti che effettuano blocchi e migliora le prestazioni evitando salti di thread non necessari.
Avvertimento
Deadlock del costruttore statico. Durante l'esecuzione di costruttori statici (cctors), il CLR mantiene un blocco. Se un costruttore statico si blocca in un'attività e la continuazione dell'attività deve eseguire il codice nello stesso tipo (o un tipo coinvolto nella catena di costruzione), la continuazione non può procedere perché il cctor blocco viene mantenuto. Evitare di bloccare completamente le chiamate all'interno di costruttori statici.
Scartamento<dell'attività>
Quando si passa un'espressione lambda asincrona a un metodo come StartNew, l'oggetto restituito è un Task<Task> oggetto (o Task<Task<TResult>>), non un semplice Taskoggetto . L'attività esterna viene completata non appena l'espressione lambda asincrona raggiunge il primo risultato.await Non aspetta che il compito interno sia completato.
public static class TaskTaskBugExample
{
public static async Task DemoAsync()
{
var sw = Stopwatch.StartNew();
// StartNew returns Task<Task>, not Task.
// The outer task completes immediately when the lambda yields.
await Task.Factory.StartNew(async () =>
{
await Task.Delay(1000);
});
// Elapsed shows ~0 seconds, not ~1 second.
Console.WriteLine($"Elapsed: {sw.Elapsed.TotalSeconds:F2}s");
}
}
Public Module TaskTaskBugExample
Public Async Function DemoAsync() As Task
Dim sw = Stopwatch.StartNew()
' StartNew returns Task(Of Task), not Task.
' The outer task completes immediately when the lambda yields.
Await Task.Factory.StartNew(Async Function()
Await Task.Delay(1000)
End Function)
' Elapsed shows ~0 seconds, not ~1 second.
Console.WriteLine($"Elapsed: {sw.Elapsed.TotalSeconds:F2}s")
End Function
End Module
Risolvere il problema in uno dei tre modi seguenti:
Utilizzare invece Run.
Task.Runrimuove automaticamente il wrapping diTask<Task>public static class TaskTaskFix1 { public static async Task DemoAsync() { var sw = Stopwatch.StartNew(); await Task.Run(async () => { await Task.Delay(1000); }); Console.WriteLine($"Elapsed: {sw.Elapsed.TotalSeconds:F2}s"); } }Public Module TaskTaskFix1 Public Async Function DemoAsync() As Task Dim sw = Stopwatch.StartNew() Await Task.Run(Async Function() Await Task.Delay(1000) End Function) Console.WriteLine($"Elapsed: {sw.Elapsed.TotalSeconds:F2}s") End Function End ModuleChiamare Unwrap sul risultato:
public static class TaskTaskFix2 { public static async Task DemoAsync() { var sw = Stopwatch.StartNew(); await Task.Factory.StartNew(async () => { await Task.Delay(1000); }).Unwrap(); Console.WriteLine($"Elapsed: {sw.Elapsed.TotalSeconds:F2}s"); } }Public Module TaskTaskFix2 Public Async Function DemoAsync() As Task Dim sw = Stopwatch.StartNew() Await Task.Factory.StartNew(Async Function() Await Task.Delay(1000) End Function).Unwrap() Console.WriteLine($"Elapsed: {sw.Elapsed.TotalSeconds:F2}s") End Function End ModuleAttendere due volte (prima l'attività esterna, quindi l'interno):
public static class TaskTaskFix3 { public static async Task DemoAsync() { var sw = Stopwatch.StartNew(); Task<Task> outerTask = Task.Factory.StartNew(async () => { await Task.Delay(1000); }); Task innerTask = await outerTask; await innerTask; Console.WriteLine($"Elapsed: {sw.Elapsed.TotalSeconds:F2}s"); } }Public Module TaskTaskFix3 Public Async Function DemoAsync() As Task Dim sw = Stopwatch.StartNew() Dim outerTask As Task(Of Task) = Task.Factory.StartNew(Async Function() Await Task.Delay(1000) End Function) Dim innerTask As Task = Await outerTask Await innerTask Console.WriteLine($"Elapsed: {sw.Elapsed.TotalSeconds:F2}s") End Function End Module
Mancato utilizzo di await in una chiamata che restituisce un task
Se si chiama un metodo che restituisce un'attività all'interno di un metodo async senza attenderne il completamento, il metodo avvia l'operazione asincrona, ma non attende che si completi. Il compilatore avvisa questo caso con CS4014 in C# e BC42358 in Visual Basic:
public static class MissingAwaitExample
{
// BAD: Task.Delay is started but never awaited.
public static async Task PauseOneSecondBuggyAsync()
{
Task.Delay(1000); // CS4014 warning
}
// GOOD: await the task.
public static async Task PauseOneSecondAsync()
{
await Task.Delay(1000);
}
}
Public Module MissingAwaitExample
' BAD: Task.Delay is started but never awaited.
Public Async Function PauseOneSecondBuggyAsync() As Task
Task.Delay(1000) ' Warning BC42358
End Function
' GOOD: Await the task.
Public Async Function PauseOneSecondAsync() As Task
Await Task.Delay(1000)
End Function
End Module
L'archiviazione del risultato in una variabile elimina l'avviso, ma non corregge il bug sottostante. Sempre await l'attività, a meno che non si voglia intenzionalmente un comportamento di tipo "fire-and-forget".