Я только что сделал любопытное замечание относительно Task.WhenAll
метода при работе на .NET Core 3.0. Я передал простую Task.Delay
задачу в качестве единственного аргумента Task.WhenAll
и ожидал, что завернутая задача будет вести себя идентично исходной задаче. Но это не так. Продолжения исходной задачи выполняются асинхронно (что желательно), а продолжения кратныTask.WhenAll(task)
оболочек выполняются синхронно один за другим (что нежелательно).
Вот демонстрация этого поведения. Четыре рабочих задачи ожидают выполнения одной и той же Task.Delay
задачи, а затем продолжат тяжелые вычисления (смоделированные 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);
Вот вывод. Четыре продолжения работают, как и ожидалось, в разных потоках (параллельно).
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
результата приводит к тому же странному поведению.
Еще одно наблюдение: я ожидал, что завертывание оболочки внутри a Task.Run
сделает продолжения асинхронными. Но этого не происходит. Продолжения строки ниже все еще выполняются в том же потоке (синхронно).
await Task.Run(async () => await Task.WhenAll(task));
Пояснение: вышеуказанные различия наблюдались в консольном приложении, работающем на платформе .NET Core 3.0. В .NET Framework 4.8 нет разницы между ожиданием исходной задачи или программой-оболочкой. В обоих случаях продолжения выполняются синхронно в одном и том же потоке.
источник
await Task.WhenAll(new[] { task });
?Task.WhenAll
Task.Delay
с100
на,1000
чтобы оно не было завершено приawait
редактировании.Ответы:
Таким образом, у вас есть несколько асинхронных методов, ожидающих одной и той же переменной задачи;
Да, эти продолжения будут называться последовательно после
task
завершения. В вашем примере каждое продолжение затягивает поток на следующую секунду.Если вы хотите, чтобы каждое продолжение выполнялось асинхронно, вам может понадобиться что-то вроде;
Так что ваши задачи возвращаются из начального продолжения и позволяют загрузке процессора работать за пределами
SynchronizationContext
.источник
Task.Yield
это хорошее решение моей проблемы. Мой вопрос, однако, больше о том, почему это происходит, а не о том, как заставить желаемое поведение.SynchronizationContext
, вызоваConfigureAwait(false)
один раз на исходное задание может быть достаточно.SynchronizationContext.Current
оно пустое. Но я просто проверил это, чтобы быть уверенным. Я добавилConfigureAwait(false)
вawait
строке, и это не имело никакого значения. Наблюдения такие же, как и ранее.Когда задача создается с использованием
Task.Delay()
, ее параметры создания устанавливаютсяNone
вместоRunContinuationsAsychronously
.Это может привести к серьезным изменениям между .net framework и .net core. Независимо от этого, это, кажется, объясняет поведение, которое вы наблюдаете. Вы также можете проверить это из копаться в исходном коде , который
Task.Delay()
является newing доDelayPromise
, который вызывает по умолчаниюTask
конструктор , не оставляя никаких вариантов создания указанных.источник
RunContinuationsAsychronously
вместо созданияNone
при создании новогоTask
объекта он стал по умолчанию ? Это объясняет некоторые мои наблюдения, но не все. В частности, это не объясняет разницу между ожиданием однойTask.WhenAll
и той же оболочки и ожиданием разных оболочек.В вашем коде следующий код находится вне повторяющегося тела.
поэтому каждый раз, когда вы запускаете следующее, он будет ждать задачи и запускать ее в отдельном потоке
но если вы запустите следующее, он будет проверять состояние
task
, поэтому он будет запускать его в одном потокено если вы переместите создание задачи рядом с
WhenAll
ним, каждая задача будет запущена в отдельном потоке.источник
Task.WhenAll
обычнымTask
, как и оригиналtask
. Обе задачи в какой-то момент завершаются: исходный в результате события таймера и составной в результате завершения исходной задачи. Почему их продолжения должны отображать другое поведение? В чем аспект одной задачи отличается от другой?