web-dev-qa-db-fra.com

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

Je viens de faire une observation curieuse concernant la méthode Task.WhenAll , lors de l'exécution sur .NET Core 3.0. J'ai passé une simple tâche Task.Delay Comme 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 suites de la tâche originale sont exécutées de manière asynchrone (ce qui est souhaitable), et les suites de plusieurs enveloppes Task.WhenAll(task) sont exécutées de manière synchrone les unes après les autres (ce qui n'est pas souhaitable).

Voici une démo de ce comportement. Quatre tâches de travail attendent la même tâche Task.Delay Pour terminer, puis continuent avec un calcul lourd (simulé par un 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 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écommente la ligne suivante await Task.WhenAll(task), la sortie est assez différente. Toutes les suites 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 worker attend un wrapper 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 workers, le comportement est identique au premier cas (continuations asynchrones).

Ma question est: pourquoi cela se produit-il? Qu'est-ce qui provoque l'exécution synchrone des suites de différents wrappers de la même tâche dans le même thread?

Remarque: encapsuler une tâche avec Task.WhenAny au lieu de Task.WhenAll Donne le même résultat étrange comportement.

Une autre observation: Je m'attendais à ce que le fait d'encapsuler le wrapper dans un Task.Run Rendrait 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 le wrapper de tâches. Dans les deux cas, les continuations sont exécutées de manière synchrone, dans le même thread.

14
Theodor Zoulias

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

var task = Task.Delay(100);

donc chaque fois que vous exécutez ce qui suit, il attendra la tâche et l'exécutera dans un thread séparé

await task;

mais si vous exécutez ce qui suit, il vérifiera l'état de task, donc il l'exécutera dans un thread

await Task.WhenAll(task);

mais si vous déplacez la création de tâche à côté de WhenAll, chaque tâche sera exécutée dans un thread distinct.

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