Je viens de faire une observation curieuse concernant la Task.WhenAll
méthode, lors de l'exécution sur .NET Core 3.0. J'ai transmis une Task.Delay
tâche simple en tant qu'argument unique à Task.WhenAll
, et je m'attendais à ce que la tâche encapsulée se comporte de manière identique à la tâche d'origine. Mais ce n'est pas le cas. Les continuations de la tâche d'origine sont exécutées de manière asynchrone (ce qui est souhaitable), et les continuations de plusieurs Task.WhenAll(task)
wrappers sont exécutées de manière synchrone l'une après l'autre (ce qui n'est pas souhaitable).
Voici une démonstration de ce comportement. Quatre tâches de travail attendent la fin de la même Task.Delay
tâche, puis poursuivent un calcul lourd (simulé par 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);
Voici la sortie. Les quatre suites 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 commente la ligne suivante await Task.WhenAll(task)
, la sortie est assez différente. Toutes les continuations s'exécutent dans le même thread, donc les calculs ne sont pas parallélisés. Chaque calcul commence après la fin 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 emballage différent. Si je définis le wrapper à l'avance:
var task = Task.WhenAll(Task.Delay(500));
... puis await
la même tâche à l'intérieur de tous les travailleurs, le comportement est identique au premier cas (suites asynchrones).
Ma question est: pourquoi cela se produit-il? Qu'est-ce qui provoque l'exécution continue des différents wrappers de la même tâche dans le même thread, de manière synchrone?
Remarque: encapsuler une tâche avec Task.WhenAny
au lieu de Task.WhenAll
résulte dans le même comportement étrange.
Autre observation: je m'attendais à ce que le wrapper à l'intérieur d'un Task.Run
rende les suites asynchrones. Mais ça n'arrive pas. Les suites 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 le .NET Framework 4.8, il n'y a aucune différence entre l'attente de la tâche d'origine ou l'encapsuleur de tâches. Dans les deux cas, les suites sont exécutées de manière synchrone, dans le même thread.
la source
await Task.WhenAll(new[] { task });
t-il si ?Task.WhenAll
Task.Delay
de100
en1000
afin qu'elle ne soit pas terminée lorsqu'elle estawait
éditée.Réponses:
Vous disposez donc de plusieurs méthodes asynchrones attendant la même variable de tâche;
Oui, ces suites seront appelées en série une fois
task
terminées. Dans votre exemple, chaque continuation monopolise ensuite le thread pendant la seconde suivante.Si vous souhaitez que chaque continuation s'exécute de manière asynchrone, vous aurez peut-être besoin de quelque chose comme;
Pour que vos tâches reviennent de la suite initiale et permettent à la charge CPU de s'exécuter en dehors de
SynchronizationContext
.la source
Task.Yield
c'est une bonne solution à mon problème. Ma question est cependant plus sur pourquoi cela se produit, et moins sur la façon de forcer le comportement souhaité.SynchronizationContext
, appelerConfigureAwait(false)
une fois sur la tâche d'origine peut être suffisant.SynchronizationContext.Current
est nulle. Mais je l'ai juste vérifié pour être sûr. J'ai ajoutéConfigureAwait(false)
dans laawait
ligne et cela n'a fait aucune différence. Les observations sont les mêmes que précédemment.Lorsqu'une tâche est créée à l'aide de
Task.Delay()
, ses options de création sont définies surNone
plutôt queRunContinuationsAsychronously
.Cela pourrait rompre le changement entre le framework .net et le core .net. Quoi qu'il en soit, cela semble expliquer le comportement que vous observez. Vous pouvez également vérifier de creuser dans le code source qui
Task.Delay()
est Newing jusqu'à unDelayPromise
qui appelle le défautTask
constructeur laissant aucune option de création spécifiées.la source
RunContinuationsAsychronously
est devenu la valeur par défaut au lieu deNone
, lors de la construction d'un nouvelTask
objet? Cela expliquerait certaines de mes observations mais pas toutes. Plus précisément, cela n'expliquerait pas la différence entre attendre le mêmeTask.WhenAll
wrapper et attendre différents wrappers.Dans votre code, le code suivant est hors du corps récurrent.
donc chaque fois que vous exécutez ce qui suit, il attendra la tâche et l'exécutera dans un thread séparé
mais si vous exécutez ce qui suit, il vérifiera l'état de
task
, donc il l'exécutera dans un threadmais si vous déplacez la création de tâche à côté,
WhenAll
elle exécutera chaque tâche dans un thread séparé.la source
Task.WhenAll
est juste un habituéTask
, comme l'originaltask
. Les deux tâches sont terminées à un moment donné, l'original à la suite d'un événement de minuterie et le composite à la suite de l'achèvement de la tâche d'origine. Pourquoi leurs suites devraient-elles afficher un comportement différent? Sous quel aspect la tâche est-elle différente de l'autre?