Почему продолжения Task.When Все выполняются синхронно? - PullRequest
14 голосов
/ 10 марта 2020

Я только что сделал любопытное наблюдение относительно метода Task.WhenAll при работе на. NET Core 3.0. Я передал простую задачу Task.Delay в качестве единственного аргумента Task.WhenAll и ожидал, что перенесенная задача будет вести себя идентично исходной задаче. Но это не так. Продолжения исходной задачи выполняются асинхронно (что желательно), а продолжения нескольких оболочек Task.WhenAll(task) выполняются синхронно одна за другой (что нежелательно).

Вот демо этого поведения. Четыре рабочих задачи ожидают завершения одной и той же задачи Task.Delay, а затем продолжат сложные вычисления (смоделированные 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);

Вот выходные данные. Четыре продолжения работают, как и ожидалось, в разных потоках (параллельно).

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

Теперь, если я прокомментирую строку await task и раскомментирую следующую строку await Task.WhenAll(task), вывод будет совсем другим. Все продолжения выполняются в одном потоке, поэтому вычисления не распараллеливаются. Каждое вычисление начинается после завершения предыдущего:

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

Удивительно, но это происходит только тогда, когда каждый работник ожидает отдельную оболочку. Если я определю оболочку заранее:

var task = Task.WhenAll(Task.Delay(500));

... и затем await одна и та же задача для всех рабочих, поведение идентично первому случаю (асинхронное продолжение).

Мой вопрос: Почему это происходит? Что заставляет синхронно выполнять продолжения разных оболочек одной и той же задачи в одном и том же потоке?

Примечание: перенос задачи с Task.WhenAny вместо Task.WhenAll приводит к тому же странному поведению.

Еще одно наблюдение: Я ожидал, что завертывание оболочки внутри Task.Run сделает продолжения асинхронными. Но этого не происходит. Продолжения строки ниже по-прежнему выполняются в том же потоке (синхронно).

await Task.Run(async () => await Task.WhenAll(task));

Уточнение: Вышеуказанные различия наблюдались в консольном приложении, работающем на. NET Платформа Core 3.0. В. NET Framework 4.8 нет разницы между ожиданием исходной задачи или программой-оболочкой. В обоих случаях продолжения выполняются синхронно в одном и том же потоке.

Ответы [ 3 ]

2 голосов
/ 10 марта 2020

Таким образом, у вас есть несколько асинхронных c методов, ожидающих одной и той же переменной задачи;

    await task;
    // CPU heavy operation

Да, эти продолжения будут вызываться последовательно после завершения task. В вашем примере каждое продолжение затем запускает поток на следующую секунду.

Если вы хотите, чтобы каждое продолжение выполнялось асинхронно, вам может понадобиться что-то вроде:

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

, чтобы ваши задачи возвращались из первоначальное продолжение, и позволить загрузке процессора работать за пределами SynchronizationContext.

1 голос
/ 11 марта 2020

Когда задача создается с использованием Task.Delay(), параметры ее создания устанавливаются на None, а не RunContinuationsAsychronously.

Это может быть разрыв между. net рамки и. net ядро. Независимо от этого, это, кажется, объясняет поведение, которое вы наблюдаете. Вы также можете проверить это, покопавшись в исходном коде, что Task.Delay() равно обновлению a DelayPromise, которое вызывает конструктор Task по умолчанию, не оставляя параметров создания.

0 голосов
/ 10 марта 2020

В вашем коде следующий код находится вне повторяющегося тела.

var task = Task.Delay(100);

, поэтому каждый раз, когда вы запускаете следующее, оно будет ждать задания и запускать его в отдельном потоке

await task;

но если вы запустите следующее, он проверит состояние task, поэтому он запустит его в одном потоке

await Task.WhenAll(task);

, но если вы переместите создание задачи в WhenAll, запускать каждую задачу в отдельном потоке.

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