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.
źródło
await Task.WhenAll(new[] { task });
?Task.WhenAll
Task.Delay
z100
na1000
, aby nie była ona ukończona poawait
edycji.Odpowiedzi:
Masz więc wiele metod asynchronicznych oczekujących na tę samą zmienną zadania;
Tak, te kontynuacje będą wywoływane szeregowo po
task
zakończeniu. W twoim przykładzie każda kontynuacja następnie przerywa wątek na następną sekundę.Jeśli chcesz, aby każda kontynuacja przebiegała asynchronicznie, możesz potrzebować czegoś takiego;
Aby Twoje zadania powróciły od początkowej kontynuacji, i pozwól, aby obciążenie procesora działało poza
SynchronizationContext
.źródło
Task.Yield
to dobre rozwiązanie mojego problemu. Moje pytanie dotyczy jednak bardziej tego, dlaczego tak się dzieje, a mniej tego, jak wymusić pożądane zachowanie.SynchronizationContext
,ConfigureAwait(false)
wystarczy raz zadzwonić do pierwotnego zadania.SynchronizationContext.Current
wartość jest zerowa. Ale właśnie to sprawdziłem. DodałemConfigureAwait(false)
wawait
linii i to nie miało znaczenia. Obserwacje są takie same jak poprzednio.Gdy zadanie jest tworzone przy użyciu
Task.Delay()
, jego opcje tworzenia są ustawione naNone
zamiastRunContinuationsAsychronously
.Może to być przełomowa zmiana między strukturą .net a rdzeniem .net. Niezależnie od tego wydaje się wyjaśniać zachowanie, które obserwujesz. Można również sprawdzić to od kopania do kodu źródłowego, który
Task.Delay()
jest newing sięDelayPromise
który wywołuje domyślnyTask
konstruktor nie pozostawiając możliwości tworzenia określonych.źródło
RunContinuationsAsychronously
stał się domyślny zamiastNone
podczas konstruowania nowegoTask
obiektu? To by wyjaśniało niektóre z moich obserwacji, ale nie wszystkie. W szczególności nie wyjaśniłoby to różnicy między oczekiwaniem na to samoTask.WhenAll
opakowanie a oczekiwaniem na inne opakowanie.W twoim kodzie poniższy kod nie występuje w treści cyklicznej.
więc za każdym razem, gdy uruchomisz następującą, będzie czekać na zadanie i uruchomi je w osobnym wątku
ale jeśli uruchomisz poniższe, sprawdzi stan
task
, więc uruchomi go w jednym wątkuale jeśli przeniesiesz tworzenie zadania obok
WhenAll
niego, uruchomisz każde zadanie w osobnym wątku.źródło
Task.WhenAll
jest po prostu normalneTask
, jak oryginałtask
. Oba zadania kończą się w pewnym momencie, oryginał w wyniku zdarzenia czasowego, a złożony w wyniku zakończenia pierwotnego zadania. Dlaczego ich kontynuacje powinny wykazywać inne zachowanie? W jakim aspekcie jedno zadanie różni się od drugiego?