Właśnie zrobiłem ciekawą obserwację dotyczącą tej Task.WhenAllmetody, gdy działałem na .NET Core 3.0. Przekazałem proste Task.Delayzadanie jako pojedynczy argument Task.WhenAlli 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.Delayzadania, 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 taski 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 awaitto 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.WhenAnyzamiast Task.WhenAllpowoduje takie samo dziwne zachowanie.
Kolejna obserwacja: spodziewałem się, że zawinięcie opakowania wewnątrz a Task.Runspowoduje, ż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.Delayz 100na 1000, aby nie była ona ukończona po awaitedycji.
await Task.WhenAll(new[] { task });?