Skip to main content

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 .

info

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 WhenAll em 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.Run sem 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 .