C# Task
Na aula async e await, cada método assíncrono devolvia uma Task ("tarefa"), e o await esperava por ela. Nesta aula, a tarefa vira a protagonista. Você vai ver o que existe dentro de uma Task, mandar uma conta pesada para outra thread com Task.Run, baixar três arquivos ao mesmo tempo com Task.WhenAll, pegar a loja que responde primeiro com Task.WhenAny e desistir de quem demora demais.
- O que é uma
Task, os seus estados (Status) e oTask<T>, que carrega um resultado. - Rodar uma conta demorada com
Task.Run, sem travar o programa principal. - Esperar várias tarefas com
Task.WhenAlle receber os resultados em ordem. - Criar uma tarefa por item com LINQ (
Select) e esperar todas. - Pegar a primeira que terminar com
Task.WhenAny, e usar isso para desistir depois de um tempo. - O que acontece com as exceções de várias tarefas (
AggregateException).
Os programas rodam com dotnet run na pasta ~/cs da Ana, num notebook com 8 núcleos. Os tempos medidos variam um pouco de uma execução para outra; quando várias tarefas rodam juntas, a ordem de algumas linhas também pode mudar.
A ideia: a senha da lanchonete
Na lanchonete, você faz o pedido e recebe uma senha. A senha não é o lanche: é a promessa de um lanche. Com ela na mão, você pode sentar, conversar, olhar o celular. Pela senha, dá para saber se o pedido ainda está na fila, se já está pronto ou se deu problema ("acabou o pão"). Quando chamam o seu número, você troca a senha pelo lanche.
Uma Task é essa senha. Um Task<string> é a senha de um string, que você recebe com await. E nada impede você de fazer vários pedidos e ficar com várias senhas ao mesmo tempo: é o que o Task.WhenAll organiza.
| Tipo | Senha de… | O await devolve |
|---|---|---|
Task | um trabalho sem resultado (gravar, enviar) | nada |
Task<int> | um int (contar, somar) | um int |
Task<string> | um string (ler, baixar) | um string |
As classes ficam em System.Threading.Tasks, que já vem incluído.
1. Os estados de uma tarefa
Task espera = Task.Delay(1000);
Console.WriteLine(espera.Status);
Console.WriteLine(espera.IsCompleted);
await espera;
Console.WriteLine(espera.Status);
Console.WriteLine(espera.IsCompleted);
Task<int> pronta = Task.FromResult(42);
Console.WriteLine($"{pronta.Status} {await pronta}");
$ dotnet run status.cs WaitingForActivation False RanToCompletion True RanToCompletion 42
- Task.Delay(1000)
- Uma tarefa que termina daqui a 1 segundo. Sem
awaitna frente, você fica com a tarefa (a senha) na variável. - espera.Status
- O estado, um enum
TaskStatus.WaitingForActivation("esperando ser ativada") quer dizer "ainda não terminou". Os principais estados finais sãoRanToCompletion("rodou até o fim", deu certo),Faulted("com defeito", lançou uma exceção) eCanceled(cancelada, aula Cancelamento). - espera.IsCompleted
truequando a tarefa terminou, de qualquer jeito. Também existemIsCompletedSuccessfully(terminou bem) eIsFaulted(deu erro).- Task.FromResult(42)
- Uma tarefa que já nasce pronta, com o resultado dentro. Útil quando um método precisa devolver
Task<int>mas, às vezes, já tem a resposta (por exemplo, guardada de uma consulta anterior). Para umaTasksem resultado,Task.CompletedTask.
Na prática, você quase nunca olha o Status: o await espera, entrega o resultado e lança a exceção, se houver. Mas ele ajuda a entender o que é uma tarefa: um objeto que diz em que pé está um trabalho.
2. Uma conta pesada: Task.Run
O Task.Delay e os métodos …Async de arquivos e da internet esperam por um "aparelho" (o relógio, o disco, a rede). Mas e se o trabalho demorado é uma conta, o próprio processador trabalhando? Aí o await sozinho não ajuda: alguém tem de fazer a conta. O Task.Run ("rodar tarefa") entrega a conta para uma thread do pool de threads do .NET, um grupo de "ajudantes" que ficam esperando trabalho (aula Threads). Num computador com vários núcleos, várias contas andam ao mesmo tempo:
using System.Diagnostics;
Console.WriteLine($"Programa principal: thread {Environment.CurrentManagedThreadId}");
Stopwatch relogio = Stopwatch.StartNew();
long a = Calcular("A");
long b = Calcular("B");
long c = Calcular("C");
long d = Calcular("D");
Console.WriteLine($"Um depois do outro: {a + b + c + d} em {relogio.Elapsed.TotalSeconds:F1} s");
relogio.Restart();
Task<long>[] tarefas =
[
Task.Run(() => Calcular("A")),
Task.Run(() => Calcular("B")),
Task.Run(() => Calcular("C")),
Task.Run(() => Calcular("D")),
];
long[] partes = await Task.WhenAll(tarefas);
Console.WriteLine($"Com Task.Run: {partes.Sum()} em {relogio.Elapsed.TotalSeconds:F1} s");
long Calcular(string nome)
{
Console.WriteLine($" {nome} na thread {Environment.CurrentManagedThreadId}");
long soma = 0;
for (int i = 0; i < 300_000_000; i++)
{
soma += i % 7;
}
return soma;
}
$ dotnet run run.cs Programa principal: thread 1 A na thread 1 B na thread 1 C na thread 1 D na thread 1 Um depois do outro: 3599999988 em 4,1 s A na thread 4 C na thread 7 B na thread 5 D na thread 8 Com Task.Run: 3599999988 em 2,0 s $ dotnet run run.cs « de novo: a ordem e o tempo mudam um pouco Programa principal: thread 1 A na thread 1 B na thread 1 C na thread 1 D na thread 1 Um depois do outro: 3599999988 em 4,1 s A na thread 4 B na thread 5 C na thread 7 D na thread 8 Com Task.Run: 3599999988 em 2,6 s
- Environment.CurrentManagedThreadId
- O número da thread que está rodando aquela linha. O programa principal é a thread 1.
- Task.Run(() => Calcular("A"))
- Recebe uma lambda com o trabalho e o coloca na fila do pool de threads. Devolve na hora um
Task<long>, porque a lambda devolvelong. Repare: oCalcularé um método comum, semasync. - A na thread 4, C na thread 7…
- Cada conta rodou numa thread diferente do pool, ao mesmo tempo que as outras. A ordem das mensagens depende de qual começou primeiro, e muda de uma execução para outra.
- await Task.WhenAll(tarefas)
- Espera as quatro (seção 3) e devolve os quatro resultados num
long[]. - 4,1 s → 2,0 s
- As contas são as mesmas, e o resultado também (
3599999988). Mas, divididas entre núcleos, terminaram em metade do tempo. Não foi 4 vezes mais rápido porque os núcleos dividem outras coisas (a memória, o calor, os outros programas abertos).
O .NET do editor roda em WebAssembly, dentro do navegador, com uma thread só. Nele, o Task.Run funciona e dá o mesmo resultado, mas todas as contas rodam na thread 1, uma depois da outra, e o tempo não cai. Para ver a diferença de velocidade, rode no seu computador. As missões desta aula usam Task.Delay, que funciona igual nos dois.
3. Várias tarefas: Task.WhenAll
A Ana vai baixar três arquivos. Cada download é uma espera (a rede), simulada aqui com Task.Delay. Em vez de baixar um depois do outro, ela começa os três e espera todos:
using System.Diagnostics;
(string Nome, int Segundos)[] arquivos = [("fotos.zip", 3), ("musica.mp3", 1), ("manual.pdf", 2)];
Stopwatch relogio = Stopwatch.StartNew();
List<Task<int>> tarefas = [];
foreach (var arquivo in arquivos)
{
tarefas.Add(BaixarAsync(arquivo.Nome, arquivo.Segundos));
}
int[] tamanhos = await Task.WhenAll(tarefas);
for (int i = 0; i < arquivos.Length; i++)
{
Console.WriteLine($"{arquivos[i].Nome}: {tamanhos[i]} MB");
}
Console.WriteLine($"Total: {tamanhos.Sum()} MB em {relogio.Elapsed.TotalSeconds:F1} s");
async Task<int> BaixarAsync(string nome, int segundos)
{
Console.WriteLine($"Baixando {nome}...");
await Task.Delay(segundos * 1000);
Console.WriteLine($"Pronto: {nome}");
return segundos * 25;
}
$ dotnet run baixar.cs Baixando fotos.zip... Baixando musica.mp3... Baixando manual.pdf... Pronto: musica.mp3 Pronto: manual.pdf Pronto: fotos.zip fotos.zip: 75 MB musica.mp3: 25 MB manual.pdf: 50 MB Total: 150 MB em 3,0 s
- (string Nome, int Segundos)[]
- Um array de tuplas: cada item tem um nome e quantos segundos o download leva.
- tarefas.Add(BaixarAsync(…))
- Chamar o método começa o download e devolve a tarefa, que vai para a lista. Ao fim do
foreach, os três downloads já estão andando. - await Task.WhenAll(tarefas)
- "Quando todas": uma tarefa que só termina quando todas da lista terminarem. Com
Task<int>, oawaitdevolve umint[]com os resultados na ordem da lista, e não na ordem em que terminaram:tamanhos[0]é sempre o das fotos, mesmo que elas tenham chegado por último. - 3,0 s
- O tempo do download mais longo, e não a soma (6 s).
Missão: este programa baixa um arquivo de cada vez: o await dentro do foreach espera cada download antes de começar o próximo. Mude-o para começar os três e esperar todos de uma vez: no laço, guarde cada tarefa na lista tarefas (sem await), e depois do laço faça int[] tamanhos = await Task.WhenAll(tarefas);. As três linhas Baixando… devem aparecer seguidas, e o total continua 150 MB.
(string Nome, int Segundos)[] arquivos = [("fotos.zip", 3), ("musica.mp3", 1), ("manual.pdf", 2)];
List<Task<int>> tarefas = [];
List<int> tamanhos = [];
foreach (var arquivo in arquivos)
{
int tamanho = await BaixarAsync(arquivo.Nome, arquivo.Segundos);
tamanhos.Add(tamanho);
}
Console.WriteLine($"Total: {tamanhos.Sum()} MB");
async Task<int> BaixarAsync(string nome, int segundos)
{
Console.WriteLine($"Baixando {nome}...");
await Task.Delay(segundos * 1000);
Console.WriteLine($"Pronto: {nome}");
return segundos * 25;
}
(string Nome, int Segundos)[] arquivos = [("fotos.zip", 3), ("musica.mp3", 1), ("manual.pdf", 2)];
List<Task<int>> tarefas = [];
foreach (var arquivo in arquivos)
{
tarefas.Add(BaixarAsync(arquivo.Nome, arquivo.Segundos));
}
int[] tamanhos = await Task.WhenAll(tarefas);
Console.WriteLine($"Total: {tamanhos.Sum()} MB");
async Task<int> BaixarAsync(string nome, int segundos)
{
Console.WriteLine($"Baixando {nome}...");
await Task.Delay(segundos * 1000);
Console.WriteLine($"Pronto: {nome}");
return segundos * 25;
}
Uma tarefa por item, com LINQ
"Uma tarefa para cada item da lista" é tão comum que costuma ser escrito numa linha, com o Select da aula Filtrar e Projetar:
(string Nome, int Segundos)[] arquivos = [("fotos.zip", 3), ("musica.mp3", 1), ("manual.pdf", 2)];
int[] tamanhos = await Task.WhenAll(arquivos.Select(a => BaixarAsync(a.Nome, a.Segundos)));
Console.WriteLine(string.Join(" + ", tamanhos) + $" = {tamanhos.Sum()} MB");
async Task<int> BaixarAsync(string nome, int segundos)
{
await Task.Delay(segundos * 1000);
return segundos * 25;
}
$ dotnet run linq.cs 75 + 25 + 50 = 150 MB
O Select transforma cada arquivo numa tarefa (começando o download), e o WhenAll recebe a sequência de tarefas.
4. A primeira que terminar: Task.WhenAny
A Ana quer saber o preço de um produto e pergunta para três lojas. Basta a primeira resposta. E se uma loja demorar demais, ela desiste:
Task<string> lojaA = ConsultarAsync("Loja A", 3000);
Task<string> lojaB = ConsultarAsync("Loja B", 1000);
Task<string> lojaC = ConsultarAsync("Loja C", 2000);
Task<string> primeira = await Task.WhenAny(lojaA, lojaB, lojaC);
Console.WriteLine($"Mais rápida: {await primeira}");
Task<string> lenta = ConsultarAsync("Loja D", 5000);
Task limite = Task.Delay(1500);
if (await Task.WhenAny(lenta, limite) == limite)
{
Console.WriteLine("Loja D: demorou demais, desisti");
}
async Task<string> ConsultarAsync(string loja, int ms)
{
await Task.Delay(ms);
return loja;
}
$ dotnet run lojas.cs Mais rápida: Loja B Loja D: demorou demais, desisti
- await Task.WhenAny(lojaA, lojaB, lojaC)
- "Quando qualquer uma": termina assim que a primeira das tarefas terminar. O resultado do
awaitnão é o texto: é a própria tarefa que venceu (umTask<string>). - await primeira
- Um segundo
await, agora na tarefa vencedora, para pegar o texto. Ela já terminou, então não há espera. - Task limite = Task.Delay(1500);
- Um "despertador" de 1,5 segundo.
- await Task.WhenAny(lenta, limite) == limite
- Uma corrida entre a loja e o despertador. Se o despertador venceu, a loja passou do tempo. É o jeito clássico de pôr um tempo limite (timeout) em qualquer tarefa.
As tarefas que perdem a corrida não param sozinhas: a Loja A e a Loja D continuam "consultando" até o programa terminar. Para parar uma tarefa de verdade, ela precisa aceitar um pedido de cancelamento (aula Cancelamento).
5. Quando uma tarefa falha
Se a tarefa lança uma exceção, ela fica guardada dentro dela (Status = Faulted) e é lançada de novo no await. Com várias tarefas no WhenAll, mais de uma pode falhar:
Task todas = Task.WhenAll(
EnviarAsync("Ana", 300, true),
EnviarAsync("Bruno", 100, false),
EnviarAsync("Carla", 200, true));
try
{
await todas;
}
catch (Exception e)
{
Console.WriteLine($"O await lançou: {e.Message}");
}
Console.WriteLine($"Erros guardados na tarefa: {todas.Exception!.InnerExceptions.Count}");
foreach (Exception e in todas.Exception.InnerExceptions)
{
Console.WriteLine($" - {e.Message}");
}
async Task EnviarAsync(string nome, int ms, bool falha)
{
await Task.Delay(ms);
if (falha)
{
throw new InvalidOperationException($"E-mail de {nome} não foi enviado");
}
Console.WriteLine($"E-mail de {nome} enviado");
}
$ dotnet run excecoes.cs E-mail de Bruno enviado O await lançou: E-mail de Carla não foi enviado Erros guardados na tarefa: 2 - E-mail de Carla não foi enviado - E-mail de Ana não foi enviado
- o WhenAll espera todas
- Mesmo com erros, ele só termina quando as três terminaram. O e-mail do Bruno foi enviado normalmente.
- catch (Exception e)
- O
awaitlança uma das exceções (aqui, a da Carla, a primeira a falhar). - todas.Exception
- A tarefa guarda todas as exceções numa
AggregateException("exceção agregada", que junta várias). A lista está emInnerExceptions("exceções internas"). O!diz ao compilador que ela não énullaqui (sabemos que houve erro).
Qual usar
| Você quer… | Use |
|---|---|
| Esperar uma tarefa e pegar o resultado | await tarefa |
| Fazer uma conta pesada sem travar o programa | await Task.Run(() => Conta()) |
| Várias esperas ao mesmo tempo, e todos os resultados | await Task.WhenAll(tarefas) |
| Uma tarefa para cada item de uma lista | await Task.WhenAll(lista.Select(x => FazerAsync(x))) |
| Só a primeira resposta | await Task.WhenAny(…), depois await na vencedora |
| Um tempo limite | Task.WhenAny(tarefa, Task.Delay(ms)) |
| Muitas contas iguais sobre uma lista grande | Parallel.For ou PLINQ (aula Parallel e PLINQ) |
| Devolver uma tarefa já pronta | Task.FromResult(valor) ou Task.CompletedTask |
Quando dá errado
List<Task> tarefas = [];
for (int i = 0; i < 3; i++)
{
tarefas.Add(Task.Run(() => Console.WriteLine($"Tarefa {i}")));
}
await Task.WhenAll(tarefas);
$ dotnet run laco.cs Tarefa 0 Tarefa 3 Tarefa 3 $ dotnet run laco.cs Tarefa 3 Tarefa 3 Tarefa 3
O que aconteceu: a lambda não guarda o valor de i no momento em que foi criada: ela guarda a variável i, que é uma só para o for inteiro. As tarefas só rodam um pouquinho depois, quando o laço já terminou e i vale 3. Às vezes uma tarefa é rápida o bastante para pegar o 0. O resultado muda a cada execução, o que torna esse erro difícil de encontrar.
Como resolver: copie o valor para uma variável dentro do laço, que é nova a cada volta: int numero = i; e use numero na lambda. (Com foreach, a variável já é nova a cada volta, desde o C# 5.) A ordem das linhas continua podendo variar, mas cada número aparece uma vez:
$ dotnet run laco2.cs Tarefa 0 Tarefa 2 Tarefa 1
CS0029: Task<int> não é int
int n = ContarAsync();
Console.WriteLine(n);
async Task<int> ContarAsync()
{
await Task.Delay(100);
return 3;
}
$ dotnet run semawait.cs /home/ana/cs/semawait.cs(1,9): error CS0029: Não é possível converter implicitamente tipo "System.Threading.Tasks.Task<int>" em "int" Ocorreu uma falha no build. Corrija os erros de build e execute novamente.
O que aconteceu: o método devolve a senha (Task<int>), e a variável espera o lanche (int).
Como resolver: int n = await ContarAsync();.
.Wait() e .Result: a exceção embrulhada
Falhar().Wait();
async Task Falhar()
{
await Task.Delay(100);
throw new InvalidOperationException("O servidor não respondeu");
}
$ dotnet run wait.cs; echo "código: $?" Unhandled exception. System.AggregateException: One or more errors occurred. (O servidor não respondeu) ---> System.InvalidOperationException: O servidor não respondeu at Program.<<Main>$>g__Falhar|0_0() in /home/ana/cs/wait.cs:line 6 --- End of inner exception stack trace --- at System.Threading.Tasks.Task.Wait(Int32 millisecondsTimeout, CancellationToken cancellationToken) at System.Threading.Tasks.Task.Wait() at Program.<Main>$(String[] args) in /home/ana/cs/wait.cs:line 1 código: 134
O que aconteceu: o .Wait() ("esperar") e o .Result ("resultado") esperam a tarefa travando a thread, como um Thread.Sleep. E, ao contrário do await, eles embrulham a exceção numa AggregateException ("ocorreram um ou mais erros"), o que complica o catch. Em aplicativos com janelas, travar a thread principal esperando uma tarefa que precisa dela pode congelar o programa para sempre. No editor do guia, que tem uma thread só, esperar assim uma tarefa que ainda não terminou nem é permitido: o programa para com PlatformNotSupportedException: Cannot wait on monitors on this runtime. ("não é possível esperar neste runtime").
Como resolver: use await, e o método que o contém vira async. O .Result só é seguro numa tarefa que você sabe que já terminou (por exemplo, depois de um await Task.WhenAll).
Referência rápida
| Código | O que faz |
|---|---|
Task / Task<T> | Um trabalho em andamento, sem / com resultado |
t.Status, t.IsCompleted, t.IsFaulted | Em que pé está |
Task.Run(() => …) | Roda o trabalho numa thread do pool |
Task.Delay(ms) | Uma tarefa que termina depois do tempo |
await Task.WhenAll(t1, t2, …) / (lista) | Espera todas; com Task<T>, devolve T[] na ordem da lista |
await Task.WhenAny(t1, t2, …) | Espera a primeira; devolve a tarefa vencedora |
Task.FromResult(v), Task.CompletedTask | Tarefas já prontas |
t.Exception.InnerExceptions | Todas as exceções guardadas (AggregateException) |
Environment.CurrentManagedThreadId | O número da thread atual |
Exercícios
1. Esperar todas
Complete para esperar todas as tarefas da lista e receber os resultados:
2. Para o pool
Complete para rodar a conta numa thread do pool:
3. A primeira
Complete para esperar só a primeira tarefa que terminar:
4. Três downloads levam 3 s, 1 s e 2 s. Começando os três e usando await Task.WhenAll, quanto tempo leva, mais ou menos?
5. Em int[] r = await Task.WhenAll(tA, tB, tC);, a tC termina primeiro. Onde está o resultado dela?
6. O que o await Task.WhenAny(t1, t2) devolve?
Missão final: a loja mais rápida, com tempo limite
Missão: a Ana pergunta o preço de um caderno para três lojas. O programa espera as três, uma por uma. Mude-o:
- use
await Task.WhenAny(loja1, loja2, loja3)para pegar a primeira que responder e mostreMais rápida:seguido do resultado dela (lembre do segundoawait, na tarefa vencedora); - depois, consulte a
Livraria Onlinecom um tempo limite de 2 segundos: se oTask.Delay(2000)vencer a corrida, mostreLivraria Online: demorou demais.
Task<string> loja1 = ConsultarAsync("Loja do Bairro", 13.50m, 2500);
Task<string> loja2 = ConsultarAsync("Papelaria Centro", 12.90m, 1000);
Task<string> loja3 = ConsultarAsync("Mercado Bom Preço", 11.75m, 1500);
Console.WriteLine(await loja1);
Console.WriteLine(await loja2);
Console.WriteLine(await loja3);
Task<string> livraria = ConsultarAsync("Livraria Online", 10.00m, 4000);
// tempo limite de 2 segundos para a livraria
async Task<string> ConsultarAsync(string loja, decimal preco, int ms)
{
await Task.Delay(ms);
return $"{loja} (R$ {preco:F2})";
}
Task<string> loja1 = ConsultarAsync("Loja do Bairro", 13.50m, 2500);
Task<string> loja2 = ConsultarAsync("Papelaria Centro", 12.90m, 1000);
Task<string> loja3 = ConsultarAsync("Mercado Bom Preço", 11.75m, 1500);
Task<string> primeira = await Task.WhenAny(loja1, loja2, loja3);
Console.WriteLine($"Mais rápida: {await primeira}");
Task<string> livraria = ConsultarAsync("Livraria Online", 10.00m, 4000);
Task limite = Task.Delay(2000);
if (await Task.WhenAny(livraria, limite) == limite)
{
Console.WriteLine("Livraria Online: demorou demais");
}
else
{
Console.WriteLine(await livraria);
}
async Task<string> ConsultarAsync(string loja, decimal preco, int ms)
{
await Task.Delay(ms);
return $"{loja} (R$ {preco:F2})";
}
A mais rápida não é a mais barata: o Mercado Bom Preço responderia meio segundo depois com R$ 11,75. Se quiser ir além: em vez da primeira resposta, espere as três com WhenAll e mostre a mais barata (os preços estão no texto; para comparar de verdade, faça o método devolver uma tupla com o nome e o decimal).
Resumo
- Uma
Taské a "senha" de um trabalho em andamento; oTask<T>carrega um resultado, que oawaitentrega. OStatusdiz em que pé ela está. Task.Runmanda uma conta pesada para uma thread do pool; várias contas assim usam vários núcleos (no editor do guia, que tem uma thread só, o tempo não cai).Task.WhenAllespera todas e devolve os resultados na ordem das tarefas; o tempo é o da mais longa. Com LINQ:Task.WhenAll(lista.Select(…)).Task.WhenAnydevolve a primeira tarefa que terminou; com umTask.Delayna corrida, vira um tempo limite.- Exceções ficam guardadas na tarefa e voltam no
await; oWhenAllguarda todas emException.InnerExceptions. - Cuidado com a variável do
fordentro da lambda, e prefiraawaita.Wait()e.Result.