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.
fonte
await Task.WhenAll(new[] { task });
?Task.WhenAll
Task.Delay
de100
para1000
para que não seja concluída quandoawait
ed.Respostas:
Portanto, você tem vários métodos assíncronos aguardando a mesma variável de tarefa;
Sim, essas continuações serão chamadas em série quando
task
concluídas. No seu exemplo, cada continuação monopoliza o encadeamento pelo segundo seguinte.Se você deseja que cada continuação seja executada de forma assíncrona, pode ser necessário algo como;
Para que suas tarefas retornem da continuação inicial e permitam que a carga da CPU seja executada fora do
SynchronizationContext
.fonte
Task.Yield
é uma boa solução para o meu problema. Minha pergunta, porém, é mais sobre por que isso está acontecendo e menos sobre como forçar o comportamento desejado.SynchronizationContext
chamadaConfigureAwait(false)
uma vez na tarefa original pode ser suficiente.SynchronizationContext.Current
é nulo. Mas eu apenas verifiquei para ter certeza. Eu adicioneiConfigureAwait(false)
naawait
linha e não fez diferença. As observações são as mesmas que anteriormente.Quando uma tarefa é criada usando
Task.Delay()
, suas opções de criação são definidas como emNone
vez deRunContinuationsAsychronously
.Isso pode estar quebrando a mudança entre a estrutura .net e o núcleo .net. Independentemente disso, parece explicar o comportamento que você está observando. Você também pode verificar isso digitando o código-fonte que
Task.Delay()
está lançando umDelayPromise
que chama oTask
construtor padrão, sem deixar opções de criação especificadas.fonte
RunContinuationsAsychronously
, o padrão se tornou o padrão ao invés deNone
construir um novoTask
objeto? Isso explicaria algumas das minhas observações, mas não todas. Especificamente, isso não explicaria a diferença entre aguardar o mesmoTask.WhenAll
invólucro e aguardar invólucros diferentes.No seu código, o código a seguir está fora do corpo recorrente.
portanto, sempre que você executar o seguinte, ele aguardará a tarefa e a executará em um encadeamento separado
mas se você executar o seguinte, ele verificará o estado de
task
, portanto, executará em um Threadmas se você mover a criação de tarefas para o lado
WhenAll
, cada tarefa será executada em um thread separado.fonte
Task.WhenAll
é apenas regularTask
, como a originaltask
. As duas tarefas estão sendo concluídas em algum momento, o original como resultado de um evento de timer e o composto como resultado da conclusão da tarefa original. Por que suas continuações exibem um comportamento diferente? Em que aspecto a tarefa é diferente da outra?