Właśnie zrobiłem ciekawą obserwację dotyczącą tej Task.WhenAll
metody, gdy działałem na .NET Core 3.0. Przekazałem proste Task.Delay
zadanie jako pojedynczy argument Task.WhenAll
i spodziewałem się, że zapakowane zadanie będzie zachowywać się identycznie jak zadanie pierwotne. Ale tak nie jest. Kontynuacje pierwotnego zadania są wykonywane asynchronicznie (co jest pożądane), a kontynuacje wielu Task.WhenAll(task)
opakowań są wykonywane synchronicznie jeden po drugim (co jest niepożądane).
Oto demonstracja tego zachowania. Cztery zadania robocze oczekują na zakończenie tego samego Task.Delay
zadania, a następnie kontynuują ciężkie obliczenia (symulowane przez 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);
Oto wynik. Cztery kontynuacje przebiegają zgodnie z oczekiwaniami w różnych wątkach (równolegle).
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
Teraz, jeśli skomentuję wiersz await task
i odkomentuję następujący wiersz await Task.WhenAll(task)
, wynik będzie zupełnie inny. Wszystkie kontynuacje są uruchomione w tym samym wątku, więc obliczenia nie są równoległe. Każde obliczenie rozpoczyna się po zakończeniu poprzedniego:
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
Zaskakująco dzieje się tak tylko wtedy, gdy każdy pracownik czeka na inne opakowanie. Jeśli z góry zdefiniuję opakowanie:
var task = Task.WhenAll(Task.Delay(500));
... a następnie await
to samo zadanie we wszystkich pracownikach, zachowanie jest identyczne jak w pierwszym przypadku (kontynuacje asynchroniczne).
Moje pytanie brzmi: dlaczego tak się dzieje? Co powoduje, że kontynuacje różnych opakowań tego samego zadania wykonują się synchronicznie w tym samym wątku?
Uwaga: zawinięcie zadania Task.WhenAny
zamiast Task.WhenAll
powoduje takie samo dziwne zachowanie.
Kolejna obserwacja: spodziewałem się, że zawinięcie opakowania wewnątrz a Task.Run
spowoduje, że kontynuacje będą asynchroniczne. Ale tak się nie dzieje. Kontynuacje poniższej linii są nadal wykonywane w tym samym wątku (synchronicznie).
await Task.Run(async () => await Task.WhenAll(task));
Wyjaśnienie: Powyższe różnice zaobserwowano w aplikacji konsoli działającej na platformie .NET Core 3.0. W .NET Framework 4.8 nie ma różnicy między oczekiwaniem na pierwotne zadanie lub na opakowanie zadania. W obu przypadkach kontynuacje są wykonywane synchronicznie, w tym samym wątku.
Task.WhenAll
Task.Delay
z 100
na 1000
, aby nie była ona ukończona po await
edycji.
await Task.WhenAll(new[] { task });
?