Я только что сделал любопытное наблюдение относительно метода 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 нет разницы между ожиданием исходной задачи или программой-оболочкой. В обоих случаях продолжения выполняются синхронно в одном и том же потоке.