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.
fuente
await Task.WhenAll(new[] { task });
?Task.WhenAll
Task.Delay
de100
a1000
para que no se complete cuando seawait
edita.Respuestas:
Entonces tiene múltiples métodos asíncronos esperando la misma variable de tarea;
Sí, estas continuaciones se llamarán en serie cuando se
task
complete. En su ejemplo, cada continuación acapara el hilo durante el siguiente segundo.Si desea que cada continuación se ejecute de forma asincrónica, puede necesitar algo como;
Para que sus tareas regresen de la continuación inicial y permitan que la carga de la CPU se ejecute fuera del
SynchronizationContext
.fuente
Task.Yield
es una buena solución a mi problema. Sin embargo, mi pregunta es más sobre por qué sucede esto y menos sobre cómo forzar el comportamiento deseado.SynchronizationContext
, llamarConfigureAwait(false)
una vez a la tarea original puede ser suficiente.SynchronizationContext.Current
es nula. Pero acabo de comprobarlo para estar seguro. AgreguéConfigureAwait(false)
en elawait
línea y no hizo ninguna diferencia. Las observaciones son las mismas que anteriormente.Cuando se crea una tarea utilizando
Task.Delay()
, sus opciones de creación se establecen enNone
lugar deRunContinuationsAsychronously
.Esto podría estar rompiendo el cambio entre .net framework y .net core. Independientemente de eso, parece explicar el comportamiento que está observando. También puede verificar esto cavando en el código fuente que se
Task.Delay()
está renovando yDelayPromise
que llama alTask
constructor predeterminado sin dejar opciones de creación especificadas.fuente
RunContinuationsAsychronously
ha convertido en el valor predeterminado en lugar deNone
cuando se construye un nuevoTask
objeto? Esto explicaría algunas de mis observaciones, pero no todas. Específicamente, no explicaría la diferencia entre esperar el mismoTask.WhenAll
contenedor y esperar diferentes contenedores.En su código, el siguiente código está fuera del cuerpo recurrente.
así que cada vez que ejecutes lo siguiente esperará la tarea y la ejecutará en un hilo separado
pero si ejecuta lo siguiente, verificará el estado de
task
, por lo que lo ejecutará en un subprocesopero si mueve la creación de tareas a su lado
WhenAll
, ejecutará cada tarea en un subproceso separado.fuente
Task.WhenAll
es solo una regularTask
, como la originaltask
. Ambas tareas se están completando en algún momento, el original como resultado de un evento de temporizador y el compuesto como resultado de la finalización de la tarea original. ¿Por qué sus continuaciones muestran un comportamiento diferente? ¿En qué aspecto es diferente una tarea de la otra?