Acabei de fazer uma observação curiosa sobre o Task.WhenAll
método, ao executar no .NET Core 3.0. Passei uma Task.Delay
tarefa 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.Delay
tarefa 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 task
e 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 await
a 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.WhenAny
vez de Task.WhenAll
resultar no mesmo comportamento estranho.
Outra observação: eu esperava que envolver o invólucro dentro de a Task.Run
tornasse 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.Delay
de 100
para 1000
para que não seja concluída quando await
ed.
await Task.WhenAll(new[] { task });
?