Erros comuns de async/await

Async/await simplifica a programação assíncrona, mas certos erros aparecem repetidamente. Este artigo descreve os cinco bugs mais comuns no código assíncrono e mostra como corrigir cada um deles.

O método assíncrono é executado de forma síncrona

Adicionar a async palavra-chave a um método não faz com que o método seja executado em um thread em segundo plano. Informa ao compilador que permita await dentro do corpo do método e envolva o valor de retorno em um Task. Quando você invoca um método assíncrono, ele é executado de forma síncrona até chegar ao primeiro await em um awaitable incompleto. Se o método não contiver expressões await ou se cada elemento awaitable já estiver concluído, o método será completado inteiramente no thread de chamada:

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

Aqui, o método retorna uma tarefa concluída imediatamente porque ela nunca cede. O compilador emite um aviso quando um método assíncrono não tem await expressões.

Se o seu objetivo for descarregar o trabalho associado à CPU em um thread do pool, use Run em vez de 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

Para obter mais diretrizes sobre quando usar Task.Run, consulte wrappers assíncronos para métodos síncronos.

Não é possível aguardar um método assíncrono nulo

Quando você converte um método síncrono void-returning em assíncrono, altere o tipo de retorno para Task. Se você deixar o tipo de retorno como void, o método se tornará "nulo assíncrono", que você não pode aguardar:

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

Os métodos assíncronos nulos servem a uma finalidade específica: manipuladores de eventos de nível superior em estruturas de interface do usuário. Fora dos manipuladores de eventos, sempre retorne Task ou Task<T> de métodos assíncronos. Os métodos assíncronos nulos têm estas desvantagens:

  • As exceções passam despercebidas. As exceções geradas em um método nulo assíncrono se propagam para o SynchronizationContext que estava ativo quando o método foi iniciado. O chamador não pode capturar essas exceções.
  • Os chamadores não podem acompanhar a conclusão. Sem um Task, não há mecanismo para saber quando a operação é concluída.
  • O teste é difícil. Você não pode aguardar o método em um teste para verificar seu comportamento.

Deadlocks devido ao bloqueio no código assíncrono

Esse bug é a causa mais comum do código assíncrono que "nunca é concluído". Isso acontece quando você bloqueia de forma síncrona (chamada Wait, Task<TResult>.Result ou GetAwaiter.GetResult) em um thread que possui um SynchronizationContext de thread único.

A sequência que causa um deadlock:

  1. O código no thread da interface (ou um thread de solicitação ASP.NET no ASP.NET mais antigo) chama um método assíncrono e bloqueia a tarefa retornada.
  2. O método assíncrono aguarda uma tarefa incompleta sem usar ConfigureAwait(false).
  3. Quando a tarefa aguardada é concluída, a continuação tenta postar de volta no SynchronizationContext original.
  4. O thread do contexto está bloqueado aguardando a conclusão da tarefa — situação de 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

Como evitar deadlocks

Use uma ou mais destas estratégias:

  • Não bloqueie. Use await em vez de .Result ou .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
    
  • Use ConfigureAwait(false) no código da biblioteca. Quando o método de biblioteca não precisar ser retomado no contexto do chamador, especifique ConfigureAwait(false) em cada 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
    

    Usar ConfigureAwait(false) informa ao runtime para não realizar o marshaling da continuação de volta para o SynchronizationContext original. Essa abordagem protege os chamadores que bloqueiam e melhora o desempenho evitando saltos de thread desnecessários.

Aviso

Deadlocks de construtor estático. O CLR mantém um bloqueio durante a execução de construtores estáticos (cctors). Se um construtor estático bloquear em relação a uma tarefa, e a continuação dessa tarefa precisar executar o código no mesmo tipo (ou em um tipo envolvido na cadeia de construção), ela não poderá prosseguir porque o bloqueio cctor está sendo mantido. Evite bloquear totalmente as chamadas dentro de construtores estáticos.

Tarefa<Desembrulhamento>

Quando você passa um lambda assíncrono para um método como StartNew, o objeto retornado é um Task<Task> (ou Task<Task<TResult>>), não um simples Task. A tarefa externa é concluída assim que o lambda assíncrono atinge seu primeiro rendimento await. Ele não aguarda a conclusão da tarefa interna:

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

Corrija esse problema de uma das três maneiras:

  • Use Run em seu lugar. Task.Run desempacota automaticamente 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
    
  • Chame Unwrap o resultado:

    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
    
  • Aguarde duas vezes (primeiro a tarefa externa, depois a parte interna):

    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
    

Espera ausente em uma chamada de retorno de tarefa

Se você chamar um método que retorna uma tarefa em um método async sem esperar, o método iniciará a operação assíncrona, mas não aguardará a conclusão. O compilador avisa sobre esse caso com CS4014 em C# e BC42358 no 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

Armazenar o resultado em uma variável suprime o aviso, mas não corrige o bug subjacente. Sempre await a tarefa, a menos que você queira intencionalmente um comportamento de disparar e esquecer.

Consulte também