Щойно я зробив цікаве спостереження щодо 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
призводить до такої ж дивної поведінки.
Ще одне зауваження: я очікував, що загортання обгортки всередину a Task.Run
зробить продовження асинхронним. Але це не відбувається. Продовження рядка нижче виконуються в тому ж потоці (синхронно).
await Task.Run(async () => await Task.WhenAll(task));
Пояснення: вищезазначені відмінності спостерігалися в програмі Console, що працює на платформі .NET Core 3.0. У .NET Framework 4.8 немає різниці між очікуванням оригінального завдання або обгорткою завдань. В обох випадках продовження виконується синхронно, в одній нитці.
Task.WhenAll
Task.Delay
з 100
на 1000
так, щоб воно не було завершено під await
ред.
await Task.WhenAll(new[] { task });
?