Acabei de fazer uma observação curiosa sobre o Task.WhenAllmétodo, ao executar no .NET Core 3.0. Passei uma Task.Delaytarefa simples como argumento único para Task.WhenAll, e esperava que a tarefa agrupada se comportasse de forma idêntica à tarefa original. Mas esse não é o caso. As continuações da tarefa original são executadas de forma assíncrona (o que é desejável) e as continuações de vários Task.WhenAll(task)wrappers são executadas de forma síncrona, uma após a outra (o que é indesejável).
Aqui está uma demonstração desse comportamento. Quatro tarefas de trabalho aguardam a mesma Task.Delaytarefa para serem concluídas e, em seguida, continue com um cálculo pesado (simulado por a Thread.Sleep).
var task = Task.Delay(500);
var workers = Enumerable.Range(1, 4).Select(async x =>
{
Console.WriteLine($"{DateTime.Now:HH:mm:ss.fff}" +
$" [{Thread.CurrentThread.ManagedThreadId}] Worker{x} before await");
await task;
//await Task.WhenAll(task);
Console.WriteLine($"{DateTime.Now:HH:mm:ss.fff}" +
$" [{Thread.CurrentThread.ManagedThreadId}] Worker{x} after await");
Thread.Sleep(1000); // Simulate some heavy CPU-bound computation
}).ToArray();
Task.WaitAll(workers);
Aqui está a saída. As quatro continuações estão sendo executadas conforme o esperado em diferentes threads (em paralelo).
05:23:25.511 [1] Worker1 before await
05:23:25.542 [1] Worker2 before await
05:23:25.543 [1] Worker3 before await
05:23:25.543 [1] Worker4 before await
05:23:25.610 [4] Worker1 after await
05:23:25.610 [7] Worker2 after await
05:23:25.610 [6] Worker3 after await
05:23:25.610 [5] Worker4 after await
Agora, se eu comentar await taske descomentar a linha a seguir await Task.WhenAll(task), a saída será bem diferente. Todas as continuações estão em execução no mesmo encadeamento, portanto, os cálculos não são paralelizados. Cada cálculo é iniciado após a conclusão do anterior:
05:23:46.550 [1] Worker1 before await
05:23:46.575 [1] Worker2 before await
05:23:46.576 [1] Worker3 before await
05:23:46.576 [1] Worker4 before await
05:23:46.645 [4] Worker1 after await
05:23:47.648 [4] Worker2 after await
05:23:48.650 [4] Worker3 after await
05:23:49.651 [4] Worker4 after await
Surpreendentemente, isso acontece apenas quando cada trabalhador aguarda um invólucro diferente. Se eu definir o wrapper antecipadamente:
var task = Task.WhenAll(Task.Delay(500));
... e awaita mesma tarefa em todos os trabalhadores, o comportamento é idêntico ao primeiro caso (continuações assíncronas).
Minha pergunta é: por que isso está acontecendo? O que faz com que as continuações de diferentes wrappers da mesma tarefa sejam executadas no mesmo encadeamento, de forma síncrona?
Nota: agrupar uma tarefa em Task.WhenAnyvez de Task.WhenAllresultar no mesmo comportamento estranho.
Outra observação: eu esperava que envolver o invólucro dentro de a Task.Runtornasse as continuações assíncronas. Mas isso não está acontecendo. As continuações da linha abaixo ainda são executadas no mesmo segmento (de forma síncrona).
await Task.Run(async () => await Task.WhenAll(task));
Esclarecimento: As diferenças acima foram observadas em um aplicativo Console em execução na plataforma .NET Core 3.0. No .NET Framework 4.8, não há diferença entre aguardar a tarefa original ou o invólucro de tarefa. Nos dois casos, as continuações são executadas de forma síncrona, no mesmo encadeamento.
Task.WhenAll
Task.Delayde 100para 1000para que não seja concluída quando awaited.
await Task.WhenAll(new[] { task });?