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.
- A diferença entre esperar parado (síncrono) e esperar fazendo outra coisa (assíncrono).
- Escrever métodos
async Taskeasync Task<T>e chamá-los comawait. - O que acontece, passo a passo, quando o programa chega num
await. - Usar
awaitdireto noProgram.cse nostatic async Task Main. - Ler e gravar arquivos com
ReadAllTextAsynceWriteAllTextAsync, e tratar exceções comtry. - Os erros mais comuns: o
awaitesquecido (CS4014),CS4032,CS1983e oasync 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
asyncavisa o compilador que dentro dele háawait. O tipo de retornoTask<string>("tarefa de string") quer dizer: "eu devolvo, logo de cara, uma tarefa; o texto vem depois, quando ela terminar". O nome termina emAsyncpor convenção, para quem chama saber que é assíncrono. - await Task.Delay(2000)
- O
Task.Delay("atraso") é a versão assíncrona doThread.Sleep: uma tarefa que termina depois de 2 segundos. Oawaitdiz "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 aTask: 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
awaittransforma aTask<string>nostringque ela carrega. - await no Program.cs
- As instruções de nível superior aceitam
awaitdireto. O compilador transforma o programa numstatic 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:
| Tempo | O que acontece | Na tela |
|---|---|---|
| 0 s | FazerCafeAsync() 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 s | TorrarPaoAsync() faz o mesmo, com 1 segundo. | Pão: começou |
| 0 s | O programa principal continua. | Enquanto isso, a Ana arruma a mesa |
| 0 s | await tarefaCafe: o café não terminou, então o programa principal pausa aqui. | |
| 1 s | O 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 s | O tempo do café acaba; FazerCafeAsync continua e termina. | Café: pronto |
| 2 s | O 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:
- Chamar um método
asynccomeça o trabalho e devolve umaTaskna hora. - O
awaitpausa só o método em que ele está, até a tarefa terminar, e entrega o resultado. - Para duas esperas acontecerem juntas, comece as duas antes de aguardar qualquer uma. Com
string cafe = await FazerCafeAsync();seguido destring 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 comum | Assíncrono | Quem 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); oawaitespera ela terminar antes da leitura. - await File.ReadAllTextAsync(…)
- Devolve
Task<string>; oawaitentrega o texto. - try { await … } catch
- Uma exceção dentro de um método assíncrono fica guardada na
Taske é lançada de novo noawait. Por isso otryfica em volta doawait, 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ção | Use |
|---|---|
| Ler ou gravar arquivos, principalmente grandes ou muitos | Os 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 travar | await Task.Delay(ms), nunca Thread.Sleep num método async |
| Várias esperas ao mesmo tempo | Comece 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 vez | Os 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());.
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ódigo | O 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 tarefa | Pausa 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, ReadAllLinesAsync | Arquivos sem travar |
try { await … } catch (…) { } | Exceções de tarefas aparecem no await |
Stopwatch.StartNew(), .Elapsed | Medir o tempo (System.Diagnostics) |
Exercícios
1. A palavra que pausa
Complete para esperar o texto do arquivo sem travar o programa:
2. O tipo de retorno
Complete o tipo de um método assíncrono que devolve um int:
3. Esperar sem travar
Complete para esperar 3 segundos dentro de um método async:
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:
- troque cada método por um
async Task<string>com nome terminado emAsync, e oThread.Sleepporawait Task.Delay; - no começo do programa, comece as três tarefas (guarde cada
Task<string>numa variável) e só depois aguarde cada uma comawait.
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; oawaitpausa 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, oawaitjá funciona (é umstatic async Task Main). - Use
Task.Delaye os métodos…Async(comoFile.ReadAllTextAsync) em vez de esperas que travam. Exceções aparecem noawait, dentro dotry. - Erros comuns: o
awaitesquecido (CS4014, a tarefa se perde),CS4032,CS1983e oasync void, que derruba o programa.