Czy słowo kluczowe Async await jest równoważne z wyrażeniem lambda ContinueWith?

81

Czy ktoś mógłby być na tyle uprzejmy, aby potwierdzić, czy poprawnie zrozumiałem słowo kluczowe Async await? (Korzystanie z wersji 3 CTP)

Jak dotąd odkryłem, że wstawienie słowa kluczowego await przed wywołaniem metody zasadniczo robi dwie rzeczy: A. Tworzy natychmiastowy powrót i B. Tworzy „kontynuację”, która jest wywoływana po zakończeniu wywołania metody async. W każdym przypadku kontynuacja jest pozostałą częścią bloku kodu metody.

Zastanawiam się więc, czy te dwa bity kodu są technicznie równoważne, a jeśli tak, czy to w zasadzie oznacza, że ​​słowo kluczowe await jest identyczne z tworzeniem ContinueWith Lambda (tj. Jest to w zasadzie skrót kompilatora do jednego)? Jeśli nie, jakie są różnice?

bool Success =
    await new POP3Connector(
        "mail.server.com", txtUsername.Text, txtPassword.Text).Connect();
// At this point the method will return and following code will
// only be invoked when the operation is complete(?)
MessageBox.Show(Success ? "Logged In" : "Wrong password");

VS

(new POP3Connector(
    "mail.server.com", txtUsername.Text, txtPassword.Text ).Connect())
.ContinueWith((success) =>
    MessageBox.Show(success.Result ? "Logged In" : "Wrong password"));
Maxim Gershkovich
źródło

Odpowiedzi:

83

Ogólna idea jest poprawna - pozostała część metody jest swego rodzaju kontynuacją.

„Szybka ścieżka” blogu ma szczegółów, w jaki sposób async/ awaitrobót transformacji kompilatora.

Różnice, z góry mojej głowy:

Słowo awaitkluczowe również wykorzystuje koncepcję „kontekstu planowania”. Kontekst planowania, SynchronizationContext.Currentjeśli istnieje, to cofanie się TaskScheduler.Current. Kontynuacja jest następnie uruchamiana w kontekście planowania. Więc bliżej przybliżenie byłoby przejść TaskScheduler.FromCurrentSynchronizationContextdo ContinueWith, spadając ponownie TaskScheduler.Currentw razie potrzeby.

Rzeczywista async/ awaitimplementacja opiera się na dopasowywaniu wzorców; używa wzorca „awaitable”, który umożliwia oczekiwanie na inne rzeczy poza zadaniami. Niektóre przykłady to asynchroniczne API WinRT, niektóre specjalne metody, takie jak Yieldobserwowalne Rx i specjalne gniazda awaitables, które nie uderzają tak mocno w GC . Zadania są potężne, ale nie są jedynymi, na które można czekać.

Przychodzi na myśl jeszcze jedna drobna różnica: jeśli oczekiwane jest już zakończone, wówczas asyncmetoda w rzeczywistości nie powraca; kontynuuje synchronicznie. Więc to trochę jak pasowanie TaskContinuationOptions.ExecuteSynchronously, ale bez problemów związanych ze stosem.

Stephen Cleary
źródło
2
bardzo dobrze powiedziane - staram się odstąpić od postów Jona, ponieważ są one o wiele bardziej obszerne niż cokolwiek, na co miałbym czas odpowiedzieć na SO, ale Stephen ma absolutną rację. WRT co jest oczekiwane (a GetAwaiter w szczególności), jego post nr 3 jest bardzo pomocny IMHO :) msmvps.com/blogs/jon_skeet/archive/2011/05/13/…
James Manning
4
Tu jest miejsce Stephena. W przypadku prostych przykładów łatwo pomyśleć, że async / await to tylko skrót do ContinueWith - jednak lubię myśleć o tym w odwrotnej kolejności. Async / await jest w rzeczywistości potężniejszym wyrażeniem tego, do czego używałeś ContinueWith. Problem polega na tym, że ContinueWith (...) używa lambd i umożliwia wykonanie przeniesienia do kontynuacji, ale inne koncepcje przepływu sterowania, takie jak pętle, są prawie niemożliwe, jeśli musisz umieścić połowę treści pętli przed ContinueWith (.. .), a druga połowa po. Skończysz z ręcznym łańcuchem kontynuacji.
Theo Yaung
7
Inny przykład, w którym async / await jest znacznie bardziej wyrazisty niż ContinueWith (...), zawiera wyjątki. Możesz czekać wiele razy w tym samym bloku try, a na każdym etapie wykonywania ich wyjątki mogą być kierowane do tego samego bloku catch (...) bez konieczności pisania ton kodu, który robi to wprost.
Theo Yaung
2
Ostatnią częścią async / await, na którą warto zwrócić uwagę, jest to, że jest to „koncepcja wyższego poziomu”, podczas gdy ContinueWith (...) jest bardziej ręczna i zawiera wyrażenia lambda, kreacje delegatów itp. Przy koncepcjach wyższego poziomu jest więcej możliwości optymalizacji - więc Na przykład, wiele oczekuje w tej samej metodzie w rzeczywistości "współdzieli" to samo zamknięcie lambda (to jest jak narzut pojedynczej lambdy), podczas gdy ContinueWith (...) pobiera narzut za każdym razem, gdy ją wywołujesz, ponieważ jawnie napisałeś lambdę, więc kompilator Ci to daje.
Theo Yaung
1
@MobyDisk: Aby wyjaśnić, awaitnadal przechwytuje SynchronizationContext.Currenttak, jak zawsze. Ale w ASP.NET Core SynchronizationContext.Currentjest null.
Stephen Cleary
8

To jest „zasadniczo”, ale wygenerowany kod robi coś więcej niż tylko to. Aby uzyskać więcej szczegółów na temat wygenerowanego kodu, gorąco polecam serię Eduasync Jona Skeeta:

http://codeblog.jonskeet.uk/category/eduasync/

W szczególności post # 7 dotyczy tego, co jest generowane (od CTP 2) i dlaczego, więc prawdopodobnie świetnie pasuje do tego, czego szukasz w tej chwili:

http://codeblog.jonskeet.uk/2011/05/20/eduasync-part-7-generated-code-from-a-simple-async-method/

EDYCJA: Myślę, że będzie to bardziej szczegółowe niż to, czego szukasz w pytaniu, ale jeśli zastanawiasz się, jak wyglądają rzeczy, gdy masz wiele oczek w metodzie, omówiono to w poście nr 9 :)

http://codeblog.jonskeet.uk/2011/05/30/eduasync-part-9-generated-code-for-multiple-awaits/

James Manning
źródło