22 votes

Pourquoi les continuations de Task.WhenAll sont-elles exécutées de manière synchrone ?

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.

4voto

Jeremy Lakeman Points 391

Vous avez donc plusieurs méthodes asynchrones qui attendent la même variable de tâche ;

    await task;
    // CPU heavy operation

Oui, ces continuations seront appelées en série lorsque task complète. Dans votre exemple, chaque continuation monopolise alors le fil de discussion pour la seconde suivante.

Si vous voulez que chaque continuation s'exécute de manière asynchrone, vous aurez besoin de quelque chose du genre ;

    await task;
    await Task.Yield().ConfigureAwait(false);
    // CPU heavy operation

Pour que vos tâches reviennent de la continuation initiale, et permettent à la charge CPU de s'exécuter en dehors de la SynchronizationContext .

1voto

Tanveer Badar Points 437

Lorsqu'une tâche est créée à l'aide de Task.Delay() ses options de création sont définies comme suit None plutôt que RunContinuationsAsychronously .

Il s'agit peut-être d'un changement de rupture entre .net framework et .net core. Quoi qu'il en soit, cela semble expliquer le comportement que vous observez. Vous pouvez également le vérifier en creusant dans le code source qui Task.Delay() es mise à jour a DelayPromise qui appelle la fonction par défaut Task constructeur ne laissant aucune option de création spécifiée.

0voto

Dans votre code, le code suivant est hors du corps récurrent.

var task = Task.Delay(100);

Ainsi, à chaque fois que vous exécutez la commande suivante, elle attendra la tâche et l'exécutera dans un thread séparé.

await task;

mais si vous exécutez la commande suivante, elle vérifiera l'état de la base de données. task afin de l'exécuter dans un seul fil.

await Task.WhenAll(task);

mais si vous déplacez la création de tâches à côté WhenAll il exécutera chaque tâche dans un Thread séparé.

var task = Task.Delay(100);
await Task.WhenAll(task);

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X