Acabo de hacer una observación curiosa sobre el Task.WhenAll
método, cuando se ejecuta en .NET Core 3.0. Le pasé una Task.Delay
tarea simple como argumento único a Task.WhenAll
, y esperaba que la tarea envuelta se comportara de manera idéntica a la tarea original. Pero este no es el caso. Las continuaciones de la tarea original se ejecutan de forma asíncrona (lo cual es deseable), y las continuaciones de varios Task.WhenAll(task)
envoltorios se ejecutan sincrónicamente una tras otra (lo cual no es deseable).
Aquí hay una demostración de este comportamiento. Cuatro tareas de los trabajadores esperan la misma Task.Delay
tarea para completar, y luego continúan con un 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);
Aquí está la salida. Las cuatro continuaciones se ejecutan como se esperaba en diferentes subprocesos (en 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
Ahora, si comento la línea await task
y descomento la siguiente línea await Task.WhenAll(task)
, el resultado es bastante diferente. Todas las continuaciones se ejecutan en el mismo hilo, por lo que los cálculos no están paralelos. Cada cálculo comienza después de completar el 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
Sorprendentemente, esto ocurre solo cuando cada trabajador espera un contenedor diferente. Si defino el contenedor por adelantado:
var task = Task.WhenAll(Task.Delay(500));
... y luego await
la misma tarea dentro de todos los trabajadores, el comportamiento es idéntico al primer caso (continuaciones asincrónicas).
Mi pregunta es: ¿por qué está pasando esto? ¿Qué hace que las continuaciones de diferentes contenedores de la misma tarea se ejecuten en el mismo hilo, sincrónicamente?
Nota: envolver una tarea en Task.WhenAny
lugar de Task.WhenAll
resultados en el mismo comportamiento extraño.
Otra observación: esperaba que envolver el contenedor dentro de a Task.Run
haría que las continuaciones fueran asincrónicas. Pero no está pasando. Las continuaciones de la siguiente línea todavía se ejecutan en el mismo hilo (sincrónicamente).
await Task.Run(async () => await Task.WhenAll(task));
Aclaración: las diferencias anteriores se observaron en una aplicación de consola que se ejecuta en la plataforma .NET Core 3.0. En .NET Framework 4.8, no hay diferencia entre esperar la tarea original o el contenedor de tareas. En ambos casos, las continuaciones se ejecutan sincrónicamente, en el mismo hilo.
Task.WhenAll
Task.Delay
de 100
a 1000
para que no se complete cuando se await
edita.
await Task.WhenAll(new[] { task });
?