C# async e await

Muitos programas passam a maior parte do tempo esperando: esperando um arquivo ser lido, uma página da internet responder, um banco de dados devolver os dados. Um programa que só espera, parado, desperdiça tempo, e num aplicativo com janelas a tela "congela". Com as palavras async ("assíncrono") e await ("aguardar"), o C# deixa o programa começar uma espera, fazer outras coisas, e continuar quando o resultado chegar. Nesta aula você vai preparar o café da manhã da Ana dos dois jeitos e ver o tempo cair de 3 para 2 segundos.

Você vai aprender
  • A diferença entre esperar parado (síncrono) e esperar fazendo outra coisa (assíncrono).
  • Escrever métodos async Task e async Task<T> e chamá-los com await.
  • O que acontece, passo a passo, quando o programa chega num await.
  • Usar await direto no Program.cs e no static async Task Main.
  • Ler e gravar arquivos com ReadAllTextAsync e WriteAllTextAsync, e tratar exceções com try.
  • Os erros mais comuns: o await esquecido (CS4014), CS4032, CS1983 e o async void.

Esta aula usa métodos, funções locais, generics e try e catch. Os programas rodam com dotnet run na pasta ~/cs da Ana. Os tempos medidos (como Levou 2,0 s) podem variar um pouco a cada execução.


A ideia: o café da manhã

A Ana vai preparar café (leva 2 minutos na cafeteira) e torrar pão (1 minuto na torradeira). Ela pode fazer de dois jeitos:

  • Síncrono ("um depois do outro"): liga a cafeteira e fica olhando até o café ficar pronto. Só então põe o pão na torradeira e fica olhando de novo. Total: 3 minutos, quase todos parada.
  • Assíncrono ("sem ficar esperando"): liga a cafeteira, põe o pão na torradeira e, enquanto as máquinas trabalham, arruma a mesa. Quando algo fica pronto, ela pega. Total: 2 minutos, o tempo da tarefa mais lenta.

Repare que a Ana continua sendo uma pessoa só. Ela não ficou mais rápida nem ganhou mais braços: só parou de ficar olhando a máquina. É exatamente isso que o await faz. Quem trabalha durante a espera é o "aparelho" (o disco, a rede, o relógio), e não o seu programa. (Fazer várias contas ao mesmo tempo, usando vários núcleos do processador, é outro assunto: as aulas Threads e Parallel e PLINQ.)


1. O jeito síncrono: esperar parado

Para simular as máquinas, os exemplos usam esperas: 2 segundos para o café e 1 para o pão. O Thread.Sleep ("dormir") trava o programa pelo tempo pedido, em milissegundos. O Stopwatch ("cronômetro", de System.Diagnostics) mede quanto tempo passou.

using System.Diagnostics;

Stopwatch relogio = Stopwatch.StartNew();
string cafe = FazerCafe();
string pao = TorrarPao();
Console.WriteLine($"Na mesa: {cafe} e {pao}");
Console.WriteLine($"Levou {relogio.Elapsed.TotalSeconds:F1} s");

string FazerCafe()
{
    Console.WriteLine("Café: começou");
    Thread.Sleep(2000);
    Console.WriteLine("Café: pronto");
    return "café";
}

string TorrarPao()
{
    Console.WriteLine("Pão: começou");
    Thread.Sleep(1000);
    Console.WriteLine("Pão: pronto");
    return "pão";
}
$ dotnet run sincrono.cs
Café: começou
Café: pronto
Pão: começou
Pão: pronto
Na mesa: café e pão
Levou 3,0 s

Tudo na ordem em que está escrito, e o tempo é a soma: 2 + 1 = 3 segundos. Durante o Thread.Sleep, o programa não faz nada, e não pode fazer nada.


2. O jeito assíncrono: async, Task e await

using System.Diagnostics;

Stopwatch relogio = Stopwatch.StartNew();
Task<string> tarefaCafe = FazerCafeAsync();
Task<string> tarefaPao = TorrarPaoAsync();
Console.WriteLine("Enquanto isso, a Ana arruma a mesa");
string cafe = await tarefaCafe;
string pao = await tarefaPao;
Console.WriteLine($"Na mesa: {cafe} e {pao}");
Console.WriteLine($"Levou {relogio.Elapsed.TotalSeconds:F1} s");

async Task<string> FazerCafeAsync()
{
    Console.WriteLine("Café: começou");
    await Task.Delay(2000);
    Console.WriteLine("Café: pronto");
    return "café";
}

async Task<string> TorrarPaoAsync()
{
    Console.WriteLine("Pão: começou");
    await Task.Delay(1000);
    Console.WriteLine("Pão: pronto");
    return "pão";
}
$ dotnet run assincrono.cs
Café: começou
Pão: começou
Enquanto isso, a Ana arruma a mesa
Pão: pronto
Café: pronto
Na mesa: café e pão
Levou 2,0 s
async Task<string> FazerCafeAsync()
Um método assíncrono. A palavra async avisa o compilador que dentro dele há await. O tipo de retorno Task<string> ("tarefa de string") quer dizer: "eu devolvo, logo de cara, uma tarefa; o texto vem depois, quando ela terminar". O nome termina em Async por convenção, para quem chama saber que é assíncrono.
await Task.Delay(2000)
O Task.Delay ("atraso") é a versão assíncrona do Thread.Sleep: uma tarefa que termina depois de 2 segundos. O await diz "enquanto ela não termina, este método fica pausado, e o programa pode fazer outras coisas". Quando o tempo passa, o método continua da linha seguinte.
return "café";
Dentro do método, você devolve o string, e não a Task: o compilador embrulha o valor na tarefa.
Task<string> tarefaCafe = FazerCafeAsync();
Chamar o método começa o trabalho e devolve a tarefa, sem esperar. Por isso o pão já começa na linha seguinte, com o café ainda na cafeteira.
string cafe = await tarefaCafe;
"Aguarde a tarefa do café e me dê o resultado". O await transforma a Task<string> no string que ela carrega.
await no Program.cs
As instruções de nível superior aceitam await direto. O compilador transforma o programa num static async Task Main (veja a seção 4).

O tempo caiu para 2 segundos, o da tarefa mais longa. E o pão ficou pronto antes do café, apesar de ter começado depois: as duas esperas aconteceram ao mesmo tempo.


3. Passo a passo: o que o await faz

O programa de cima, com o relógio ao lado:

TempoO que aconteceNa tela
0 sFazerCafeAsync() começa, mostra a mensagem e chega no await Task.Delay(2000). Ele pausa e devolve a tarefa, ainda não terminada.Café: começou
0 sTorrarPaoAsync() faz o mesmo, com 1 segundo.Pão: começou
0 sO programa principal continua.Enquanto isso, a Ana arruma a mesa
0 sawait tarefaCafe: o café não terminou, então o programa principal pausa aqui.
1 sO tempo do pão acaba. TorrarPaoAsync continua de onde parou, mostra a mensagem e termina: a tarefa do pão está completa.Pão: pronto
2 sO tempo do café acaba; FazerCafeAsync continua e termina.Café: pronto
2 sO await tarefaCafe recebe "café". O await tarefaPao nem precisa esperar: o pão já estava pronto.Na mesa: café e pão

Três regras para lembrar:

  1. Chamar um método async começa o trabalho e devolve uma Task na hora.
  2. O await pausa só o método em que ele está, até a tarefa terminar, e entrega o resultado.
  3. Para duas esperas acontecerem juntas, comece as duas antes de aguardar qualquer uma. Com string cafe = await FazerCafeAsync(); seguido de string pao = await TorrarPaoAsync();, o pão só começaria depois do café, e voltaria a levar 3 segundos.

4. Métodos sem resultado e o Main assíncrono

Um método assíncrono que não devolve valor tem o tipo Task (sem o <T>), assim como um método comum seria void:

Método comumAssíncronoQuem chama escreve
void Salvar()async Task SalvarAsync()await SalvarAsync();
string Ler()async Task<string> LerAsync()string t = await LerAsync();
int Contar()async Task<int> ContarAsync()int n = await ContarAsync();

E quem usa await também precisa ser async. Por isso o async "sobe" pelos métodos até o começo do programa. No Program.cs com instruções de nível superior, o compilador cuida disso. No estilo com Main (aula Program.cs e o Main), escreva static async Task Main:

class Program
{
    static async Task Main()
    {
        await Task.Delay(500);
        Console.WriteLine("Main assíncrono");
    }
}
$ dotnet run main.cs
Main assíncrono

O Main assíncrono existe desde o C# 7.1 C# 7.1. Em código antigo, você pode ver MetodoAsync().Wait() ou .Result dentro de um Main comum: isso trava a thread até a tarefa terminar e, em aplicativos com janelas, pode travar o programa para sempre. Prefira o await.


5. Arquivos de verdade: ReadAllTextAsync

O Task.Delay só simula uma espera. Na vida real, as esperas são de disco e de rede, e o .NET tem versões …Async de quase tudo o que espera: File.ReadAllTextAsync, File.WriteAllTextAsync, File.ReadAllLinesAsync, StreamReader.ReadLineAsync (aulas Arquivos e Ler e Escrever), HttpClient.GetStringAsync para baixar páginas, e os métodos dos bancos de dados.

await File.WriteAllTextAsync("diario.txt", "Hoje estudei async e await.\n");
string texto = await File.ReadAllTextAsync("diario.txt");
Console.Write(texto);

try
{
    string outro = await LerMaiusculasAsync("nao-existe.txt");
    Console.WriteLine(outro);
}
catch (FileNotFoundException e)
{
    Console.WriteLine($"Erro: {e.Message}");
}

async Task<string> LerMaiusculasAsync(string nome)
{
    string conteudo = await File.ReadAllTextAsync(nome);
    return conteudo.ToUpper();
}
$ dotnet run diario.cs
Hoje estudei async e await.
Erro: Could not find file '/home/ana/cs/nao-existe.txt'.
await File.WriteAllTextAsync(…)
Grava o arquivo sem travar o programa durante a gravação. Devolve uma Task (sem resultado); o await espera ela terminar antes da leitura.
await File.ReadAllTextAsync(…)
Devolve Task<string>; o await entrega o texto.
try { await … } catch
Uma exceção dentro de um método assíncrono fica guardada na Task e é lançada de novo no await. Por isso o try fica em volta do await, como num método comum.

Num programa de console tão pequeno, a diferença de velocidade não aparece. Ela aparece quando há muitas esperas juntas (baixar 20 páginas ao mesmo tempo, aula Task) e em programas com janelas e servidores web, que precisam continuar respondendo enquanto esperam.

Missão: rode e veja o aviso CS4014: o programa termina sem mostrar Salvo!, porque ninguém esperou a tarefa. Ponha await antes de SalvarAsync(); para a saída ficar Salvando..., Salvo! e Fim do programa, sem avisos.

SalvarAsync();
Console.WriteLine("Fim do programa");

async Task SalvarAsync()
{
    Console.WriteLine("Salvando...");
    await Task.Delay(1000);
    await File.WriteAllTextAsync("dados.txt", "importante");
    Console.WriteLine("Salvo!");
}
await SalvarAsync();
Console.WriteLine("Fim do programa");

async Task SalvarAsync()
{
    Console.WriteLine("Salvando...");
    await Task.Delay(1000);
    await File.WriteAllTextAsync("dados.txt", "importante");
    Console.WriteLine("Salvo!");
}

Quando usar async

SituaçãoUse
Ler ou gravar arquivos, principalmente grandes ou muitosOs métodos …Async com await
Internet, banco de dados, qualquer coisa que responde "depois"async/await: é para isso que eles existem
Esperar um tempo sem travarawait Task.Delay(ms), nunca Thread.Sleep num método async
Várias esperas ao mesmo tempoComece todas e aguarde com await Task.WhenAll(…) (aula Task)
Uma conta demorada (o processador trabalhando)await Task.Run(…) (aula Task) ou Parallel
Um programa curto que faz uma coisa por vezOs métodos comuns também servem; o assíncrono não deixa nada mais rápido sozinho

Quando dá errado

CS4014: faltou o await, e o programa terminou antes
$ dotnet run salvar.cs
/home/ana/cs/salvar.cs(1,1): warning CS4014: Como esta chamada não é aguardada, a execução do método atual continua antes da conclusão da chamada. Considere aplicar o operador 'await' ao resultado da chamada.
Salvando...
Fim do programa
$ ls
salvar.cs

O que aconteceu: é o programa da missão de cima. Sem await, o SalvarAsync() começou, pausou no Task.Delay e devolveu a tarefa, que ninguém aguardou. O programa principal seguiu, mostrou Fim do programa e terminou, levando a tarefa junto: o dados.txt nem foi criado (o ls confirma). O aviso diz exatamente isso: "como esta chamada não é aguardada, a execução continua antes da conclusão da chamada".

Como resolver: await SalvarAsync();. Trate o CS4014 como erro: uma tarefa sem await quase sempre é um engano.

CS4032: await num método que não é async
Console.WriteLine(Ler());

string Ler()
{
    string texto = await File.ReadAllTextAsync("dados.txt");
    return texto;
}
$ dotnet run semasync.cs
/home/ana/cs/semasync.cs(5,20): error CS4032: O operador "await" pode somente ser usado em um método assíncrono. Considere a possibilidade de marcar este método com o modificador "async" e alterar seu tipo de retorno para "Task<string>".

Ocorreu uma falha no build. Corrija os erros de build e execute novamente.

O que aconteceu: o compilador já diz o conserto (em inglês, The 'await' operator can only be used within an async method).

Como resolver: async Task<string> LerAsync(), e quem chama passa a usar await: Console.WriteLine(await LerAsync());.

Apareceu System.Runtime.CompilerServices.AsyncTaskMethodBuilder…
Console.WriteLine(LerAsync());
Console.WriteLine(await LerAsync());

async Task<string> LerAsync()
{
    await Task.Delay(100);
    return "olá";
}
$ dotnet run tarefa.cs
System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1+AsyncStateMachineBox`1[System.String,Program+<<<Main>$>g__LerAsync|0_0>d]
olá

O que aconteceu: na primeira linha, faltou o await, e o WriteLine mostrou a tarefa em si (o nome interno do tipo que o compilador criou), e não o resultado. Não há erro nem aviso, porque escrever uma Task é permitido.

Como resolver: se aparecer um nome comprido com Task ou AsyncStateMachineBox na tela, procure o await que faltou.

CS1983: o tipo de retorno de um método async
Console.WriteLine(await DobroAsync(21));

async int DobroAsync(int n)
{
    await Task.Delay(100);
    return n * 2;
}
$ dotnet run tipo.cs
/home/ana/cs/tipo.cs(1,19): error CS1061: ‘int’ não contém uma definição para "GetAwaiter" e não foi possível encontrar nenhum método de extensão "GetAwaiter" que aceite um primeiro argumento do tipo ‘int’ (você está se esquecendo de usar uma diretiva ou uma referência de assembly?)
/home/ana/cs/tipo.cs(3,11): error CS1983: O tipo de retorno de um método assíncrono precisa ser nulo, Task, Task<T>, um tipo semelhante à tarefa, IAsyncEnumerable<T> ou IAsyncEnumerator<T>

Ocorreu uma falha no build. Corrija os erros de build e execute novamente.

O que aconteceu: um método async precisa devolver uma tarefa (o "nulo" da tradução é o void). O segundo erro é o principal; o primeiro é consequência: um int não pode ser aguardado, porque não é uma tarefa (o GetAwaiter é o que o await procura).

Como resolver: async Task<int> DobroAsync(int n). Quando houver vários erros, comece pelo que fala da causa, não necessariamente pelo primeiro da lista.

async void: a exceção que derruba o programa
Explodir();
await Task.Delay(500);
Console.WriteLine("Fim");

async void Explodir()
{
    await Task.Delay(100);
    throw new InvalidOperationException("Deu errado!");
}
$ dotnet run avoid.cs; echo "código: $?"
Unhandled exception. System.InvalidOperationException: Deu errado!
   at Program.<<Main>$>g__Explodir|0_0() in /home/ana/cs/avoid.cs:line 8
   at System.Threading.Tasks.Task.<>c.<ThrowAsync>b__124_1(Object state)
   at System.Threading.ThreadPoolWorkQueue.Dispatch()
   at System.Threading.PortableThreadPool.WorkerThread.WorkerThreadStart()
   at System.Threading.Thread.StartCallback()
código: 134

O que aconteceu: um método async void não devolve tarefa nenhuma, então ninguém pode aguardá-lo nem pegar a sua exceção com try. A exceção estoura "solta" e derruba o programa inteiro, sem chegar no Fim.

Como resolver: use async Task, sempre. O async void existe só para eventos de botões e janelas (aula Eventos), em que o tipo do evento obriga.


Referência rápida

CódigoO que faz
async Task MetodoAsync()Método assíncrono sem resultado
async Task<T> MetodoAsync()Método assíncrono que devolve um T (com return valor;)
await tarefaPausa o método até a tarefa terminar e entrega o resultado
Task<T> t = MetodoAsync();Começa o trabalho sem esperar
await Task.Delay(ms)Espera sem travar
static async Task Main()Main assíncrono (no Program.cs de nível superior, automático)
File.ReadAllTextAsync, WriteAllTextAsync, ReadAllLinesAsyncArquivos sem travar
try { await … } catch (…) { }Exceções de tarefas aparecem no await
Stopwatch.StartNew(), .ElapsedMedir o tempo (System.Diagnostics)

Exercícios

1. A palavra que pausa

Complete para esperar o texto do arquivo sem travar o programa:

string texto = File.ReadAllTextAsync("notas.txt");

2. O tipo de retorno

Complete o tipo de um método assíncrono que devolve um int:

async ContarAlunosAsync()

3. Esperar sem travar

Complete para esperar 3 segundos dentro de um método async:

await Task.(3000);

4. O que acontece na linha Task<string> t = BaixarAsync();?

5. Dois métodos assíncronos levam 2 s e 1 s. Quanto tempo leva string a = await A(); string b = await B();?

6. Por que evitar async void?

Missão final: o café da manhã sem esperar

Missão: o café da manhã está no jeito síncrono, um depois do outro. Transforme-o no jeito assíncrono:

  1. troque cada método por um async Task<string> com nome terminado em Async, e o Thread.Sleep por await Task.Delay;
  2. no começo do programa, comece as três tarefas (guarde cada Task<string> numa variável) e só depois aguarde cada uma com await.

As três mensagens começou devem aparecer seguidas, no começo, e a última linha deve ser Na mesa: café, pão e suco.

string cafe = FazerCafe();
string pao = TorrarPao();
string suco = FazerSuco();
Console.WriteLine($"Na mesa: {cafe}, {pao} e {suco}");

string FazerCafe()
{
    Console.WriteLine("Café: começou");
    Thread.Sleep(2000);
    Console.WriteLine("Café: pronto");
    return "café";
}

string TorrarPao()
{
    Console.WriteLine("Pão: começou");
    Thread.Sleep(1000);
    Console.WriteLine("Pão: pronto");
    return "pão";
}

string FazerSuco()
{
    Console.WriteLine("Suco: começou");
    Thread.Sleep(1500);
    Console.WriteLine("Suco: pronto");
    return "suco";
}
Task<string> tarefaCafe = FazerCafeAsync();
Task<string> tarefaPao = TorrarPaoAsync();
Task<string> tarefaSuco = FazerSucoAsync();
string cafe = await tarefaCafe;
string pao = await tarefaPao;
string suco = await tarefaSuco;
Console.WriteLine($"Na mesa: {cafe}, {pao} e {suco}");

async Task<string> FazerCafeAsync()
{
    Console.WriteLine("Café: começou");
    await Task.Delay(2000);
    Console.WriteLine("Café: pronto");
    return "café";
}

async Task<string> TorrarPaoAsync()
{
    Console.WriteLine("Pão: começou");
    await Task.Delay(1000);
    Console.WriteLine("Pão: pronto");
    return "pão";
}

async Task<string> FazerSucoAsync()
{
    Console.WriteLine("Suco: começou");
    await Task.Delay(1500);
    Console.WriteLine("Suco: pronto");
    return "suco";
}

Depois de rodar a solução, repare na ordem dos pronto: pão (1 s), suco (1,5 s) e café (2 s), a ordem das esperas, e não a do código. Se quiser ir além: ponha um Stopwatch e compare o tempo dos dois jeitos (no síncrono, 4,5 s; no assíncrono, 2 s).


Resumo

  • Assíncrono é esperar sem ficar parado: o programa começa uma espera (disco, rede, tempo), faz outras coisas e continua quando o resultado chega.
  • Um método async Task<T> devolve uma tarefa na hora; o await pausa só aquele método até a tarefa terminar e entrega o resultado.
  • Para esperas acontecerem juntas, comece todas as tarefas antes de aguardar. O tempo total vira o da mais longa.
  • Quem usa await é async; no Program.cs de nível superior, o await já funciona (é um static async Task Main).
  • Use Task.Delay e os métodos …Async (como File.ReadAllTextAsync) em vez de esperas que travam. Exceções aparecem no await, dentro do try.
  • Erros comuns: o await esquecido (CS4014, a tarefa se perde), CS4032, CS1983 e o async void, que derruba o programa.