Bug comuni di async/await

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:

  1. 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.
  2. Il metodo asincrono attende un'attività incompleta senza usare ConfigureAwait(false).
  3. Al termine dell'attività attesa, la continuazione tenta di effettuare un postback all'oggetto originale SynchronizationContext.
  4. 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 await invece di .Result o .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 Module
    
  • Usare ConfigureAwait(false) nel codice della libreria. Quando il metodo di libreria non deve riprendere nel contesto del chiamante, specificare ConfigureAwait(false) in ogni await:

    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 Module
    

    L'uso di ConfigureAwait(false) indica al runtime di non effettuare il marshalling della continuazione all'originale SynchronizationContext. 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.Run rimuove automaticamente il wrapping di Task<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 Module
    
  • Chiamare 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 Module
    
  • Attendere 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".

Vedere anche