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-te como corrigir cada um deles.

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

Adicionar a async palavra-chave a um método não faz com que o método seja executado numa thread em segundo plano. Instrui o compilador a permitir await dentro do corpo do método e a encapsular o valor de retorno em um Task. Quando invocas um método assíncrono, ele corre de forma síncrona até chegar ao primeiro await num aguardável incompleto. Se o método não contiver expressões await, ou se todos os awaitables que aguarda já estiverem completos, então o método termina inteiramente na 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 devolve imediatamente uma tarefa concluída porque nunca resulta. O compilador emite um aviso quando um método assíncrono não tem await expressões.

Se o seu objetivo é transferir trabalho limitado pela CPU para um thread 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 mais orientações sobre quando usar Task.Run, veja Envelopes assíncronas para métodos síncronos.

Não posso esperar por um método void assíncrono

Quando converter um método que retorna void síncrono para assíncrono, altere o tipo de retorno para Task. Se deixares o tipo de retorno como void, o método torna-se "async void", o que não podes esperar:

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 void assíncronos servem um propósito específico: gestores de eventos de topo em frameworks de interface. Fora dos gestores de eventos, deve sempre retornar Task ou Task<T> de métodos assíncronos. Os métodos void assíncronos têm estas desvantagens:

  • Exceções ficam sem ser notadas. Exceções lançadas num método void assíncrono propagam-se para o SynchronizationContext que estava ativo quando o método começou. Quem chama não consegue detetar estas exceções.
  • Os chamadores não podem rastrear a conclusão. Sem um Task, não há mecanismo para saber quando a operação termina.
  • Os testes são difíceis. Não podes esperar que o método seja feito num teste para verificar o seu comportamento.

Deadlocks devido ao bloqueio em código assíncrono

Este bug é a causa mais comum de código assíncrono que "nunca termina". Acontece quando bloqueias (chamas Wait, Task<TResult>.Result, ou GetAwaiter.GetResult) de forma síncrona num thread que tem um único thread SynchronizationContext.

A sequência que causa um impasse:

  1. O código no thread UI (ou num thread de pedido 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 termina, a continuação tenta voltar ao original SynchronizationContext.
  4. O tópico desse contexto fica bloqueado à espera que a tarefa seja concluída — impasse.
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 impasses

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
    
  • Uso ConfigureAwait(false) em código de biblioteca. Quando o seu método de biblioteca não precisar de retomar 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
    

    Using ConfigureAwait(false) indica ao tempo de execução para não enviar a continuação de volta ao original SynchronizationContext. Esta abordagem protege os chamadores que bloqueiam e melhora o desempenho ao evitar saltos desnecessários entre threads.

Advertência

Interbloqueios do construtor estático O CLR mantém um bloqueio enquanto executa os construtores estáticos (cctors). Se um construtor estático bloqueia numa tarefa, e a continuação dessa tarefa precisar de executar código do mesmo tipo (ou de um tipo envolvido na cadeia de construção), a continuação não pode avançar porque o cctor bloqueio é mantido. Evite bloquear chamadas dentro de construtores estáticos por completo.

Desdobramento de tarefa<>

Quando passa um lambda assíncrono para um método como StartNew, o objeto devolvido é um Task<Task> (ou Task<Task<TResult>>), não um simples Task. A tarefa externa conclui-se assim que o lambda assíncrono atinge o seu primeiro await de espera. Não espera que a tarefa interior termine:

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

Resolver este problema de uma de três formas:

  • Utilize Run em substituição. Task.Run desencapsula 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
    
  • Avalie 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
    
  • Espere duas vezes (primeiro a tarefa exterior, depois a interior):

    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
    

Desaparecidos aguardam uma chamada de retorno de tarefa

Se chamar um método de retorno de tarefa num async método sem esperar, o método inicia a operação assíncrona mas não espera que esta seja concluída. O compilador avisa sobre este caso com CS4014 em C# e BC42358 em 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

Guardar o resultado numa variável suprime o aviso, mas não resolve o bug subjacente. Sempre await a tarefa, a menos que queiras intencionalmente um comportamento de "disparar e esquecer".

Consulte também