Jaka jest różnica między zwrotem anulowania a zwróceniem zadania?

130

Patrząc na różne przykłady C # Async CTP, widzę niektóre funkcje asynchroniczne, które zwracają void, i inne, które zwracają nieogólne Task. Rozumiem, dlaczego zwracanie a Task<MyType>jest przydatne do zwracania danych do dzwoniącego po zakończeniu operacji asynchronicznej, ale funkcje, które widziałem, które mają typ Taskzwracania, nigdy nie zwracają żadnych danych. Dlaczego nie wrócić void?

James Cadd
źródło

Odpowiedzi:

215

Odpowiedzi SLaks i Killercam są dobre; Pomyślałem, że po prostu dodam trochę więcej kontekstu.

Twoje pierwsze pytanie dotyczy zasadniczo tego, jakie metody można oznaczyć async.

Sposób oznaczone asyncmoże powrócić void, Taskalbo Task<T>. Jakie są między nimi różnice?

Task<T>Powrocie sposób asynchroniczny może być oczekiwany, a po zakończeniu wykonywania zadania będzie to propozycja górę T.

ZA TaskPowrocie sposób asynchroniczny może być oczekiwany, a gdy finalizuje zadaniowe, kontynuacja zadania jest zaplanowane do uruchomienia.

voidPowrocie sposób asynchroniczny nie może być oczekiwany; jest to metoda „odpal i zapomnij”. Działa asynchronicznie i nie możesz powiedzieć, kiedy się skończy. To więcej niż trochę dziwne; jak mówi SLaks, normalnie zrobiłbyś to tylko podczas tworzenia asynchronicznej obsługi zdarzeń. Zdarzenie jest wyzwalane, program obsługi wykonuje; nikt nie będzie „czekał” na zadanie zwrócone przez moduł obsługi zdarzeń, ponieważ programy obsługi zdarzeń nie zwracają zadań, a nawet gdyby tak było, jaki kod użyłby zadania do czegoś? Zwykle to nie kod użytkownika przekazuje kontrolę do programu obsługi w pierwszej kolejności.

Twoje drugie pytanie w komentarzu dotyczy zasadniczo tego, co można awaitedytować:

Jakie rodzaje metod można awaitedytować? Czy można edytować metodę zwracającą pustkę await?

Nie, nie można oczekiwać metody zwracającej pustkę. Kompilator tłumaczy await M()na wywołanie funkcji M().GetAwaiter(), gdzie GetAwaitermoże być metodą instancji lub metodą rozszerzającą. Oczekiwana wartość musi być wartością, za którą można dostać awaitera; oczywiście metoda zwracająca void nie tworzy wartości, z której można uzyskać awaiter.

Taskmetody zwrotne mogą dawać oczekiwane wartości. Przewidujemy, że osoby trzecie będą chciały tworzyć własne implementacje Taskpodobnych obiektów, na które można czekać, a Ty będziesz mógł na nie czekać. Jednak nie będzie można deklarować asyncmetod, które zwracają coś innego niż void, Tasklub Task<T>.

(AKTUALIZACJA: Moje ostatnie zdanie może zostać sfałszowane przez przyszłą wersję C #; istnieje propozycja zezwalająca na zwracanie typów innych niż typy zadań dla metod asynchronicznych).

(AKTUALIZACJA: wspomniana powyżej funkcja została wprowadzona do języka C # 7).

Eric Lippert
źródło
7
+1 Myślę, że jedyne, czego brakuje, to różnica w sposobie traktowania wyjątków w metodach asynchronicznych zwracających pustkę.
João Angelo
10
@JamesCadd: Załóżmy, że jakaś praca asynchroniczna zgłasza wyjątek. Kto to łapie? Kod, który uruchomił zadanie asynchroniczne, nie znajduje się już na stosie - może nawet nie znajdować się w tym samym wątku - a wyjątki zakładają, że wszystkie bloki catch / final znajdują się na stosie . Więc co robisz? Informacje o wyjątku przechowujemy w zadaniu, abyś mógł je później sprawdzić. Ale jeśli metoda zwraca void, wówczas nie ma zadania dostępnego dla kodu użytkownika. To, jak dokładnie radzimy sobie z tą sytuacją, było przedmiotem pewnych kontrowersji i nie pamiętam w tej chwili, na co zdecydowaliśmy.
Eric Lippert
8
Właściwie zadałem to pytanie Stephenowi Toubowi w BUILD. W .NET 4.0 nieobserwowane nieobsłużone wyjątki w zadaniach ostatecznie powodowały awarię procesu, gdy TPL wykryje, że nie zostały zaobserwowane. W 4.5 zmienili domyślne zachowanie, tak że niezauważone wyjątki będą nadal zgłaszane za pośrednictwem zdarzenia TaskScheduler :: UnobservedTaskException, ale nie będą już powodować awarii procesu. Jeśli chcesz zachować stare zachowanie 4.0, możesz ponownie włączyć opcję <runtime> <ThrowUnobservedTaskExceptions enabled = "true" /> </runtime>. Najprawdopodobniej zmiana została wprowadzona właśnie po to, aby wspierać metody „uruchom i zapomnij” dla metod asynchronicznych z pustką.
Drew Marsh
4
async voidmetody zgłaszają wyjątek do tego, SynchronizationContextktóry był aktywny w momencie rozpoczęcia wykonywania. Jest to podobne do zachowania (synchronicznych) programów obsługi zdarzeń. @DrewMarsh: plikUnobservedTaskException and runtime mają zastosowanie tylko do metod zadań asynchronicznych „ i zapomnij” , a nie do async voidmetod.
Stephen Cleary,
1
Odnośnik do informacji dotyczących obsługi wyjątków asynchronicznych: blogs.msdn.com/b/pfxteam/archive/2012/04/12/10293335.aspx#11
Luke Puplett
23

W przypadku, gdy dzwoniący chce poczekać na zadanie lub dodać kontynuację.

W rzeczywistości jedynym powodem powrotu voidjest to, że nie możesz wrócić, Taskponieważ piszesz procedurę obsługi zdarzeń.

SLaks
źródło
Pomyślałem, że można poczekać na metody, które zwracają również typ void - czy mógłbyś trochę rozwinąć?
James Cadd,
1
Nie, nie możesz. Jeśli metoda powróci void, nie masz możliwości przejścia do zadania, które generuje. (Właściwie nie jestem pewien, czy to w ogóle generuje Task)
SLaks
18

Metody zwracają się Taski Task<T>można je komponować - co oznacza, że ​​można awaitje znaleźć wewnątrz plikuasync metody.

asynczwracające metody voidnie są kompozowalne, ale mają dwie inne ważne właściwości:

  1. Mogą być używane jako programy obsługi zdarzeń.
  2. Reprezentują operację asynchroniczną „najwyższego poziomu”.

Druga kwestia jest ważna, gdy masz do czynienia z kontekstem, który utrzymuje liczbę oczekujących operacji asynchronicznych.

Kontekst ASP.NET jest jednym z takich kontekstów; jeśli używasz Taskmetod asynchronicznych bez oczekiwania na nie z asyncvoid metody , żądanie ASP.NET zostanie ukończone zbyt wcześnie.

Innym kontekstem jest ten, AsyncContextktóry napisałem dla testów jednostkowych (dostępny tutaj ) - AsyncContext.Runmetoda śledzi liczbę zaległych operacji i zwraca, gdy wynosi zero.

Stephen Cleary
źródło
12

Typ Task<T>jest typem konia roboczego biblioteki zadań równoległych (TPL), reprezentuje koncepcję „pewnej pracy / zadania, które Tw przyszłości przyniesie wynik typu ”. Pojęcie „praca, która zakończy się w przyszłości, ale nie zwróci żadnego wyniku” jest reprezentowane przez nieogólny typ zadania.

Dokładny sposób, w jaki zostanie utworzony wynik typu, Toraz szczegóły dotyczące realizacji określonego zadania; praca może zostać przeniesiona do innego procesu na komputerze lokalnym, do innego wątku itp. Zadania TPL są zwykle rozdzielane do wątków roboczych z puli wątków w bieżącym procesie, ale ten szczegół implementacji nie jest fundamentalny dla Task<T>typu; raczej Task<T>może reprezentować dowolną operację o dużym opóźnieniu, która generuje plik T.

Na podstawie Twojego komentarza powyżej:

Te awaitśrodki wyrażenie „ocenić to wyrażenie, aby uzyskać obiekt reprezentujący prace, które zostaną w przyszłości produkować wynik. Zapisz się pozostałą część obecnej metody jako oddzwaniania związanego z kontynuacją tego zadania. Raz, że zadanie jest produkowany i tył wezwanie jest zarejestrowany, natychmiast zwróć kontrolę rozmówcy ”. Jest to przeciwieństwo / w przeciwieństwie do zwykłego wywołania metody, co oznacza „pamiętaj, co robisz, uruchamiaj tę metodę, aż zostanie całkowicie zakończona, a następnie kontynuuj od miejsca, w którym przerwałeś, teraz znając wynik metody”.


Edycja: Powinienem zacytować artykuł Erica Lipperta z października 2011 r. MSDN Magazine, ponieważ był to dla mnie bardzo pomocny w zrozumieniu tych rzeczy.

Więcej informacji i białych stron można znaleźć tutaj .

Mam nadzieję, że to pomoże.

Księżycowy rycerz
źródło