Task.WhenAll | Threads | CSharp
Task.WhenAll| Threads | CSharp
O que é
Task.WhenAll é um método usado para aguardar a conclusão de várias Tasks ao mesmo tempo.
Ele retorna uma nova task que só termina quando todas as tasks informadas terminarem. Quando todas possuem o mesmo tipo de retorno, o resultado final é um array com os resultados. Esse comportamento aparece no material de Stephen Cleary: Task.WhenAll espera todas as tasks e, quando elas retornam o mesmo tipo, devolve um array com os resultados .
Task.WhenAll não executa automaticamente em paralelo.
Ele apenas aguarda várias tasks que já foram disparadas.
Ideia central
Use Task.WhenAll quando:
- você já possui várias operações independentes
- quer iniciar todas sem esperar uma por uma
- quer reduzir o tempo total de espera
- precisa aguardar todas antes de continuar
Exemplo conceitual:
-
errado para concorrência:
- espera a primeira operação terminar
- depois a segunda
- depois a terceira
-
correto com
WhenAll:- inicia as três
- aguarda todas juntas
Sintaxe básica
Task task1 = Task.Delay(1000);
Task task2 = Task.Delay(2000);
Task task3 = Task.Delay(1000);
await Task.WhenAll(task1, task2, task3);
Esse é exatamente o uso base descrito no livro: várias tasks são passadas para Task.WhenAll, e o await continua apenas quando todas tiverem terminado .
Quando todas retornam valor
Task<int> task1 = Task.FromResult(3);
Task<int> task2 = Task.FromResult(5);
Task<int> task3 = Task.FromResult(7);
int[] resultados = await Task.WhenAll(task1, task2, task3);
// resultados = { 3, 5, 7 }
O resultado final é um array com os valores de cada task, como mostrado na referência .
Exemplo prático
using System;
using System.Threading.Tasks;
class Program
{
static async Task Main()
{
Task<string> tarefa1 = BuscarNomeAsync();
Task<int> tarefa2 = BuscarIdadeAsync();
Task<decimal> tarefa3 = BuscarSaldoAsync();
await Task.WhenAll(tarefa1, tarefa2, tarefa3);
string nome = await tarefa1;
int idade = await tarefa2;
decimal saldo = await tarefa3;
Console.WriteLine($"Nome: {nome}");
Console.WriteLine($"Idade: {idade}");
Console.WriteLine($"Saldo: {saldo}");
}
static async Task<string> BuscarNomeAsync()
{
await Task.Delay(1000);
return "Adolfo";
}
static async Task<int> BuscarIdadeAsync()
{
await Task.Delay(1500);
return 30;
}
static async Task<decimal> BuscarSaldoAsync()
{
await Task.Delay(800);
return 1250.75m;
}
}
Por que isso é melhor do que awaits sequenciais
Veja a diferença:
Sequencial
string nome = await BuscarNomeAsync();
int idade = await BuscarIdadeAsync();
decimal saldo = await BuscarSaldoAsync();
Aqui, a segunda operação só começa depois da primeira terminar, e a terceira só começa depois da segunda.
Concorrente com WhenAll
Task<string> nomeTask = BuscarNomeAsync();
Task<int> idadeTask = BuscarIdadeAsync();
Task<decimal> saldoTask = BuscarSaldoAsync();
await Task.WhenAll(nomeTask, idadeTask, saldoTask);
string nome = await nomeTask;
int idade = await idadeTask;
decimal saldo = await saldoTask;
Aqui, todas começam antes do await Task.WhenAll.
Regra importante
Task.WhenAll não cria paralelismo por si só.
Ele apenas coordena tasks que já foram iniciadas.
Ou seja:
- ele não “joga tudo em threads diferentes”
- ele não garante execução em paralelo real
- ele apenas espera todas terminarem
Isso é importante porque muita gente confunde:
- concorrência assíncrona
- paralelismo
- multithreading
A referência usada deixa isso claro ao separar concorrência, multithreading e processamento paralelo como conceitos diferentes .
Cenários ideais para usar
Chamadas HTTP independentes
Task<string> produtoTask = httpClient.GetStringAsync("/produto");
Task<string> estoqueTask = httpClient.GetStringAsync("/estoque");
Task<string> precoTask = httpClient.GetStringAsync("/preco");
await Task.WhenAll(produtoTask, estoqueTask, precoTask);
Consultas independentes
Task<Cliente> clienteTask = ObterClienteAsync();
Task<List<Pedido>> pedidosTask = ObterPedidosAsync();
Task<List<Endereco>> enderecosTask = ObterEnderecosAsync();
await Task.WhenAll(clienteTask, pedidosTask, enderecosTask);
Processamento de vários arquivos
Task<byte[]> a = LerArquivoAsync("a.txt");
Task<byte[]> b = LerArquivoAsync("b.txt");
Task<byte[]> c = LerArquivoAsync("c.txt");
await Task.WhenAll(a, b, c);
Quando não usar
Evite Task.WhenAll quando:
- uma operação depende do resultado da outra
- existe ordem obrigatória
- há acesso concorrente inseguro a estado compartilhado
- disparar tudo ao mesmo tempo pode sobrecarregar banco, API ou memória
Exemplo de dependência:
var cliente = await BuscarClienteAsync();
var pedidos = await BuscarPedidosDoClienteAsync(cliente.Id);
Aqui não faz sentido usar WhenAll, porque a segunda operação depende da primeira.
Exceções no Task.WhenAll
Esse é um dos pontos mais importantes.
Se qualquer task falhar, a task retornada por Task.WhenAll entra em estado de falha. Se várias falharem, todas as exceções ficam agregadas na task retornada. Porém, ao dar await, normalmente apenas uma exceção é lançada diretamente; para inspecionar todas, é preciso olhar a propriedade Exception da task agregadora .
Exemplo simples
using System;
using System.Threading.Tasks;
class Program
{
static async Task Main()
{
Task t1 = Erro1Async();
Task t2 = Erro2Async();
try
{
await Task.WhenAll(t1, t2);
}
catch (Exception ex)
{
Console.WriteLine($"Exceção capturada: {ex.GetType().Name}");
}
}
static async Task Erro1Async()
{
await Task.Delay(500);
throw new InvalidOperationException("Falha 1");
}
static async Task Erro2Async()
{
await Task.Delay(500);
throw new ArgumentException("Falha 2");
}
}
Capturando todas as exceções
using System;
using System.Threading.Tasks;
class Program
{
static async Task Main()
{
Task t1 = Erro1Async();
Task t2 = Erro2Async();
Task allTasks = Task.WhenAll(t1, t2);
try
{
await allTasks;
}
catch
{
foreach (Exception ex in allTasks.Exception!.InnerExceptions)
{
Console.WriteLine($"{ex.GetType().Name}: {ex.Message}");
}
}
}
static async Task Erro1Async()
{
await Task.Delay(500);
throw new InvalidOperationException("Falha 1");
}
static async Task Erro2Async()
{
await Task.Delay(500);
throw new ArgumentException("Falha 2");
}
}
Cancelamento
Se uma ou mais tasks forem canceladas, o comportamento final depende da combinação entre:
- tasks concluídas com sucesso
- tasks com erro
- tasks canceladas
Na prática:
- se houver erro, o resultado final tende a ser falha
- se não houver erro, mas houver cancelamento, o resultado pode ser cancelado
Exemplo:
using System;
using System.Threading;
using System.Threading.Tasks;
class Program
{
static async Task Main()
{
using CancellationTokenSource cts = new();
Task t1 = OperacaoAsync("A", 1000, cts.Token);
Task t2 = OperacaoAsync("B", 3000, cts.Token);
cts.CancelAfter(1500);
try
{
await Task.WhenAll(t1, t2);
}
catch (OperationCanceledException)
{
Console.WriteLine("Operação cancelada.");
}
}
static async Task OperacaoAsync(string nome, int delay, CancellationToken ct)
{
await Task.Delay(delay, ct);
Console.WriteLine($"{nome} concluída");
}
}
Armadilha comum com LINQ
Esse ponto é muito relevante.
Ao usar LINQ para gerar tasks, a enumeração pode ainda não ter sido materializada. A recomendação mostrada por Stephen Cleary é transformar a sequência em coleção com ToArray() antes de chamar WhenAll, pois isso deixa o fluxo mais claro e garante a avaliação da sequência .
Melhor abordagem
async Task<string> DownloadAllAsync(HttpClient client, IEnumerable<string> urls)
{
var downloads = urls.Select(url => client.GetStringAsync(url));
Task<string>[] downloadTasks = downloads.ToArray();
string[] htmlPages = await Task.WhenAll(downloadTasks);
return string.Concat(htmlPages);
}
Armadilha com estado compartilhado
Task.WhenAll com várias operações concorrentes pode gerar problema se todas alterarem o mesmo estado.
O livro mostra exatamente esse risco: iniciar várias modificações concorrentes sobre a mesma variável pode exigir sincronização, dependendo do contexto de execução .
Exemplo problemático
private static int _contador = 0;
static async Task IncrementarAsync()
{
await Task.Delay(100);
_contador++;
}
static async Task ExecutarAsync()
{
Task t1 = IncrementarAsync();
Task t2 = IncrementarAsync();
Task t3 = IncrementarAsync();
await Task.WhenAll(t1, t2, t3);
}
Isso pode parecer inocente, mas alterar estado compartilhado de forma concorrente pode gerar inconsistência.
Padrão recomendado
Um padrão limpo e seguro costuma ser este:
Task<ResultadoA> tarefaA = MetodoAAsync();
Task<ResultadoB> tarefaB = MetodoBAsync();
Task<ResultadoC> tarefaC = MetodoCAsync();
await Task.WhenAll(tarefaA, tarefaB, tarefaC);
ResultadoA a = await tarefaA;
ResultadoB b = await tarefaB;
ResultadoC c = await tarefaC;
Vantagens:
- todas as operações começam cedo
- código fica legível
- você centraliza a espera
- depois acessa os resultados com clareza
Exemplo completo com API
using System;
using System.Net.Http;
using System.Threading.Tasks;
class Program
{
static async Task Main()
{
using HttpClient httpClient = new();
Task<string> postTask = httpClient.GetStringAsync("https://jsonplaceholder.typicode.com/posts/1");
Task<string> userTask = httpClient.GetStringAsync("https://jsonplaceholder.typicode.com/users/1");
Task<string> todoTask = httpClient.GetStringAsync("https://jsonplaceholder.typicode.com/todos/1");
await Task.WhenAll(postTask, userTask, todoTask);
Console.WriteLine("Post:");
Console.WriteLine(await postTask);
Console.WriteLine();
Console.WriteLine("User:");
Console.WriteLine(await userTask);
Console.WriteLine();
Console.WriteLine("Todo:");
Console.WriteLine(await todoTask);
}
}
Boas práticas
- inicie primeiro as tasks independentes
- use
await Task.WhenAll(...)para sincronizar o ponto de continuação - não use
WhenAllem operações dependentes entre si - cuidado com acesso a variáveis compartilhadas
- trate exceções conscientemente
- materialize sequências LINQ com
ToArray()quando fizer sentido - não use
Task.Runsem necessidade só para “forçar async” - avalie limites de concorrência quando houver alto volume
Task.WhenAll vs Task.WhenAny
Task.WhenAll
- espera todas terminarem
- ideal quando todas importam
Task.WhenAny
- espera a primeira terminar
- ideal quando basta a primeira resposta
A própria referência separa esses dois casos: WhenAll para esperar todas e WhenAny para reagir à primeira que terminar .
Resumo prático
Task.WhenAll serve para coordenar várias operações assíncronas independentes.
Pense assim:
- você dispara tudo primeiro
- depois espera tudo junto
- se todas derem certo, segue com os resultados
- se uma falhar, o conjunto falha
- se várias falharem, todas ficam registradas na task agregadora
Exemplo final enxuto
Task<string> nomeTask = BuscarNomeAsync();
Task<int> idadeTask = BuscarIdadeAsync();
Task<decimal> saldoTask = BuscarSaldoAsync();
await Task.WhenAll(nomeTask, idadeTask, saldoTask);
Console.WriteLine(await nomeTask);
Console.WriteLine(await idadeTask);
Console.WriteLine(await saldoTask);
Fonte de apoio: Concurrency in C# Cookbook, Second Edition, especialmente a seção sobre Task.WhenAll e tratamento de exceções em tarefas agregadas .