Je viens de faire une observation curieuse concernant le Task.WhenAll
lorsqu'il est exécuté sous .NET Core 3.0. J'ai passé un simple Task.Delay
comme un seul argument pour Task.WhenAll
et je m'attendais à ce que la tâche enveloppée se comporte de la même manière que la tâche originale. Mais ce n'est pas le cas. Les continuations de la tâche originale sont exécutées de manière asynchrone (ce qui est souhaitable), et les continuations des multiples tâches de la tâche enveloppée sont exécutées de manière asynchrone. Task.WhenAll(task)
Les wrappers sont exécutés de manière synchrone les uns après les autres (ce qui n'est pas souhaitable).
Voici un Démonstration de ce comportement. Quatre tâches de travailleur attendent le même Task.Delay
pour terminer la tâche, et ensuite continuer avec un calcul lourd (simulé par une 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);
Voici le résultat. Les quatre continuations s'exécutent comme prévu dans différents threads (en parallèle).
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
Maintenant, si je commente la ligne await task
et décommentez la ligne suivante await Task.WhenAll(task)
le résultat est très différent. Toutes les continuations sont exécutées dans le même thread, donc les calculs ne sont pas parallélisés. Chaque calcul commence après l'achèvement du précédent :
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
Étonnamment, cela ne se produit que lorsque chaque travailleur attend un wrapper différent. Si je définis le wrapper en amont :
var task = Task.WhenAll(Task.Delay(500));
...et ensuite await
la même tâche dans tous les workers, le comportement est identique au premier cas (continuations asynchrones).
Ma question est la suivante : pourquoi cela se produit-il ? Qu'est-ce qui fait que les continuations de différents wrappers de la même tâche s'exécutent dans le même thread, de manière synchrone ?
Nota: envelopper une tâche avec Task.WhenAny
au lieu de Task.WhenAll
donne lieu au même comportement étrange.
Une autre observation : Je m'attendais à ce que le fait de placer le wrapper dans un fichier Task.Run
rendrait les continuations asynchrones. Mais ce n'est pas le cas. Les continuations de la ligne ci-dessous sont toujours exécutées dans le même thread (de manière synchrone).
await Task.Run(async () => await Task.WhenAll(task));
Clarification : Les différences ci-dessus ont été observées dans une application Console exécutée sur la plate-forme .NET Core 3.0. Sur la plateforme .NET Framework 4.8, il n'y a pas de différence entre l'attente de la tâche originale ou de la tâche enveloppante. Dans les deux cas, les continuations sont exécutées de manière synchrone, dans le même thread.