Menu

async await em C#: Task, Task<T>, WhenAll e armadilhas comuns

async e await permitem que código C# espere operações lentas sem bloquear uma thread. Veja como um método async executa, Task e Task<T>, executar operações independentes ao mesmo tempo com Task.WhenAll, exceções em código async e por que async void e .Result causam bugs e deadlocks.

Esta página tem editores executáveis - edite, execute e veja a saída na hora.

A maioria das operações lentas de um programa são esperas: um servidor web responder, um banco de dados devolver linhas, um arquivo ser lido. async e await permitem que um método pause durante essa espera sem manter uma thread refém, e depois continue de onde parou. O código continua sendo lido de cima para baixo como código comum.

Um primeiro método async

Um método async retorna Task (sem resultado) ou Task<T> (um resultado do tipo T). Dentro dele, await espera outra task e dá o resultado dela.

Saída:

Tea 2.50, cake 4.00, total 6.50

GetPriceAsync diz return 2.50m, e o compilador envolve isso na Task<decimal> que o método retorna. await desembrulha de novo. Task.Delay é a versão async de Thread.Sleep: completa depois que o tempo passa, sem bloquear uma thread enquanto isso.

Desde o C# 7.1, o próprio Main pode ser async, e é assim que você escreveria este programa no .NET moderno:

static async Task Main()
{
    decimal tea = await GetPriceAsync("tea");
    Console.WriteLine(tea);
}

Os exemplos executáveis desta página chamam um método async RunAsync a partir de um Main comum com .GetAwaiter().GetResult(), que é o que o compilador gera para um Main async. Em um app de console isso é seguro; a seção sobre deadlock explica por que a mesma chamada é perigosa em código de interface.

O que acontece em um await

Um método async executa de forma síncrona até o primeiro await sobre uma task não terminada. Nesse ponto ele retorna uma Task para quem chamou, e o resto do método vira uma continuação que executa quando a task aguardada termina.

Saída:

Calling DownloadAsync
  Download: starting
DownloadAsync returned a task; doing other work
  Download: finished
Download awaited

"Download: starting" é impresso antes de DownloadAsync retornar, porque tudo até o primeiro await executa na thread de quem chamou. Depois o método devolve uma task não terminada, quem chamou segue em frente, e "Download: finished" só aparece quando a espera acaba. Chamar um método async o inicia; aguardá-lo é como você espera o resultado.

async não cria uma thread. Enquanto o método está pausado, não há nenhuma thread esperando por ele, e é por isso que um servidor pode ter milhares de requisições esperando um banco de dados com só algumas threads. Para trabalho pesado de CPU que deve executar em outra thread, use Task.Run, assunto de tasks.

Executando operações ao mesmo tempo com Task.WhenAll

Aguardar uma chamada depois da outra é sequencial: cada uma só começa depois que a anterior terminou. Quando as operações não dependem umas das outras, inicie todas e depois aguarde-as juntas com Task.WhenAll:

Exemplo de saída:

Sequential: 160 units in ~900 ms
Concurrent: 160 units in ~300 ms

A versão sequencial leva a soma das três esperas, a concorrente mais ou menos o tempo da mais lenta. Task.WhenAll retorna os resultados na mesma ordem das tasks que você passou, não importa qual terminou primeiro. Ele também funciona com uma lista, que é a forma usual de "buscar cada item": await Task.WhenAll(ids.Select(id => FetchAsync(id))).

Task.WhenAny é a contraparte que completa assim que a primeira task completa, útil para timeouts e para "a primeira resposta vence".

Exceções em código async

Uma exceção lançada dentro de um método async é guardada na task retornada e relançada quando a task é aguardada, então um try/catch comum em volta do await funciona:

Saída:

Task created, completed yet: False
Caught ArgumentOutOfRangeException for userId
WhenAll rethrew the first failure
All failures: 1
The other task still succeeded: profile 7

Chamar LoadProfileAsync(-1) não lançou nada: a exceção fica na task até que algo a aguarde. Uma task que ninguém nunca aguarda esconde completamente a exceção, mais um motivo para sempre aguardar o que você inicia. Com WhenAll, await relança a primeira exceção, enquanto a propriedade Exception da task combinada (uma AggregateException) guarda todas. Ler ok.Result ali não tem problema porque ok já completou.

async void: só para event handlers

Um método async também pode retornar void. Evite isso em todo lugar, menos em event handlers:

// Bad: the caller cannot await it or catch its exceptions.
static async void SaveAsync(Order order)
{
    await db.InsertAsync(order);   // if this throws, the process may crash
}

// Good: return Task, so callers can await and handle errors.
static async Task SaveAsync(Order order)
{
    await db.InsertAsync(order);
}

// Acceptable: an event handler must return void.
private async void SaveButton_Click(object sender, EventArgs e)
{
    try { await SaveAsync(currentOrder); }
    catch (Exception ex) { ShowError(ex); }
}

Com async void não existe task para aguardar, então quem chama segue em frente antes de o trabalho terminar, e uma exceção lançada lá dentro é disparada diretamente no contexto de sincronização (ou no thread pool), onde nenhum try/catch de quem chamou consegue alcançá-la. Em um app de console ou de servidor, isso normalmente encerra o processo. Dentro de um event handler async void, capture tudo você mesmo.

Um erro relacionado é chamar um método async sem await. O compilador avisa (CS4014), e o método executa em segundo plano sem nada observando o resultado ou os erros dele.

Deadlocks com .Result e .Wait()

Bloquear em uma task com .Result ou .Wait() é onde o código async mais dá errado. Em uma aplicação com contexto de sincronização (WinForms, WPF, MAUI, ASP.NET clássico), a sequência é:

  1. A thread de interface chama GetDataAsync().Result e bloqueia, esperando a task.
  2. Dentro de GetDataAsync, um await termina. Por padrão, a continuação precisa executar no contexto em que começou: a thread de interface.
  3. A thread de interface está bloqueada no passo 1, então a continuação nunca executa, então a task nunca completa, então o passo 1 nunca termina.

O app congela sem nenhuma exceção. Apps de console e o ASP.NET Core não têm esse contexto, e é por isso que o mesmo código funciona em um programa de teste e trava em um app desktop. As soluções:

  • Use await em toda a cadeia de chamadas em vez de bloquear ("async de ponta a ponta"). Event handlers podem ser async void para isso.
  • Em código de biblioteca que não precisa voltar ao contexto de quem chamou, escreva await SomethingAsync().ConfigureAwait(false);. A continuação então executa no thread pool em vez do contexto capturado, e a biblioteca evita uma troca de thread desnecessária. Isso só protege quem bloqueia se todo await da cadeia fizer o mesmo, então trate como boa prática de biblioteca, não como solução para o .Result.

Código de aplicação no ASP.NET Core não precisa de ConfigureAwait(false), já que não há contexto para onde voltar.

Erros comuns

  • Aguardar chamadas independentes uma por uma. Inicie-as e depois faça await Task.WhenAll(...).
  • Métodos async void. Retorne Task; deixe async void para event handlers.
  • .Result e .Wait() em código de interface ou de ASP.NET clássico. Eles causam deadlock; use await.
  • Esquecer o await. O trabalho executa sem ninguém observar, e as exceções dele desaparecem.
  • Envolver I/O em Task.Run. Uma API async como File.ReadAllTextAsync ou HttpClient.GetStringAsync já libera a thread; Task.Run só acrescenta um salto pelo thread pool.
  • Supor que async significa "executa em outra thread". Significa "pode pausar sem bloquear". Use Task.Run para trabalho limitado pela CPU.

Perguntas frequentes

Como async e await funcionam em C#?

Marcar um método como async permite que ele use await. Quando o método chega a um await sobre uma task que ainda não terminou, ele retorna imediatamente para quem chamou, devolvendo uma Task que representa o resto do trabalho. Quando a operação aguardada termina, o método continua depois do await. Nenhuma thread fica bloqueada enquanto ele espera.

Qual a diferença entre Task e Task<T>?

Task representa uma operação que não produz valor, a versão async de um método void. Task<T> representa uma que produz um T: await sobre uma Task<int> dá o int. Um método async declarado como async Task<int> só escreve return 42;, e o compilador envolve o valor na task.

Como executar várias operações async ao mesmo tempo?

Inicie todas primeiro e depois aguarde-as juntas: var a = GetUserAsync(); var b = GetOrdersAsync(); await Task.WhenAll(a, b);. Escrever await GetUserAsync(); await GetOrdersAsync(); executa uma depois da outra, então o tempo total é a soma, em vez do tempo da mais longa.

Por que async void é ruim em C#?

Um método async void não pode ser aguardado, então quem chama não sabe quando ele termina, e uma exceção lançada dentro dele não pode ser capturada por quem chamou: ela é disparada no contexto de sincronização e normalmente derruba o processo. Retorne Task. async void só existe para event handlers, cuja assinatura exige void.

Por que .Result ou .Wait() causam deadlock?

Em um app com contexto de sincronização (WinForms, WPF, ASP.NET clássico), .Result bloqueia a thread de interface ou da requisição enquanto a continuação do método aguardado espera para executar nessa mesma thread. Cada uma espera a outra para sempre. Use await em toda a cadeia em vez de bloquear, e use ConfigureAwait(false) dentro de código de biblioteca.

O Main pode ser async em C#?

Sim, desde o C# 7.1: static async Task Main() ou static async Task<int> Main(). As instruções de nível superior (C# 9) podem usar await diretamente. Em código mais antigo, o equivalente é chamar MainAsync().GetAwaiter().GetResult() a partir de um Main normal, o que é seguro ali porque um app de console não tem contexto de sincronização.

Coddy programming languages illustration

Aprenda a programar com o Coddy

COMEÇAR