2015-12-15 18 views
7

Próbuję objąć główką wszystkie elementy Async, które zostały dodane do środowiska .NET w nowszych wersjach. Rozumiem niektóre z nich, ale szczerze mówiąc, osobiście uważam, że nie ułatwia to pisania kodu asynchronicznego. Uważam, że jest to raczej mylące przez większość czasu i trudniejsze do odczytania niż bardziej konwencjonalne podejścia, które stosowaliśmy przed pojawieniem się asynchronizacji/oczekiwania.Jaki jest sens oczekiwania na DoSomethingAsync

W każdym razie moje pytanie jest proste. Widzę wiele takich kodów:

var stream = await file.readAsStreamAsync() 

Co tu się dzieje? Nie jest to równoznaczne z wywołaniem tylko blokuje wariant metody, tj

var stream = file.readAsStream() 

Jeśli tak, to jaki jest sens w stosowaniu go tutaj jak to? To nie czyni kodu łatwiejszym do odczytania, więc proszę powiedz mi, czego mi brakuje.

+1

Nie sprawia, że ​​kod asynchroniczny jest łatwiejszy do odczytania niż kod synchronizacji, ale pomyśl o tym, jak napiszesz ten sam kod asynchroniczny bez słowa kluczowego "await" ... To trudniejsze do odczytania (i napisania). Twoje przykłady są * funkcjonalnie * ekwiwalentne, ale pierwsza nie blokuje wątku tylko po to, by czekać na I/O. –

+1

Zaraz, o czym mówisz, kiedy mówisz "bardziej konwencjonalne podejścia"? To brzmi tak, jakbyś nigdy nie używał asynchronicznego I/O, ale raczej opublikował swoją pracę w oddzielnym wątku i wykonał tam synchroniczne operacje we/wy. Czy mam rację? – Luaan

+0

Powiązane: [Dlaczego warto używać Async/Await Over Normal Threading lub Tasks?] (Http://stackoverflow.com/questions/18298946/why-use-async-await-over-normal-threading-lub-tasks). – GolezTrol

Odpowiedz

5

Wynik dla obu połączeń jest taki sam.

Różnica polega na tym, że var stream = file.readAsStream() zablokuje wywołujący wątek do zakończenia operacji.

Jeśli połączenie zostało wykonane w aplikacji GUI z wątku interfejsu użytkownika, aplikacja zostanie zablokowana do momentu zakończenia operacji wejścia/wyjścia.

Jeśli połączenie zostało wykonane w aplikacji serwera, zablokowany wątek nie będzie w stanie obsłużyć innych przychodzących żądań.Pula wątków będzie musiała utworzyć nowy wątek, aby "zastąpić" zablokowany, co jest kosztowne. Skalowalność ucierpi.

Z drugiej strony var stream = await file.readAsStreamAsync() nie będzie blokować żadnego wątku. Wątek interfejsu użytkownika w aplikacji GUI może utrzymywać odpowiedź aplikacji, wątek roboczy w aplikacji serwera może obsługiwać inne żądania.

Po zakończeniu operacji asynchronicznej system operacyjny powiadomi pulę wątków, a reszta metody zostanie wykonana.

Aby umożliwić całą tę "magię", metoda z asynchronizacją/oczekiwaniem zostanie wkompilowana w automat stanów. Async/await pozwala na uproszczenie skomplikowanego kodu asynchronicznego.

+1

To ma sens, dziękuję! Kod, o którym mowa, jest w kontrolerze ASP.NET MVC, choć zastanawiam się, co tam jest dobrego? Pod koniec dnia odpowiedź HTTP na klienta może zostać wysłana tylko wtedy, gdy plik (lub cokolwiek, co robisz) zostanie w pełni przetworzony. Co ten wątek powinien zrobić w międzyczasie? Czy nie wystarczy usiąść i poczekać, aż operacja zostanie zakończona, czy może zostanie udostępniona inna próba przychodząca, podczas gdy operacja jest nadal w toku? – user3700562

+0

@ user3700562 Wątek może obsługiwać inne żądania. Nie zobaczysz różnicy w wydajności za pomocą pojedynczego żądania, ale gdy istnieją setki równoczesnych żądań, blokowanie operacji wejścia/wyjścia spowoduje marnowanie zasobów i pogorszy wydajność. –

2

Co tu się dzieje? Czy nie jest to równoznaczne z wywołaniem metody blokowania metody , tj.

Nie, to nie jest połączenie blokujące. Jest to cukier syntaktyczny używany przez kompilator do tworzenia maszyny stanu, która w środowisku wykonawczym będzie używana do asynchronicznego wykonywania kodu.

Sprawia, że ​​kod jest bardziej czytelny i prawie podobny do kodu działającego synchronicznie.

+1

Byłbym wdzięczny, gdyby ci spadkobiercy mogli wyjaśnić zło. Dzięki – Christos

+0

'czekaj' sugeruje, a OP zakłada, że ​​czeka tam na zakończenie asynchronicznego wywołania. To czekanie nastąpi w wątku wywołującym, blokując go. Myślę, że twoja odpowiedź może być prawdziwa, ale nie wyjaśnia dobrze, co się właściwie dzieje. Gdybym zadał pytanie OP (i mógłbym to zrobić), nie skorzystałbym z tej odpowiedzi i nadal nie byłbym pewien, dlaczego od razu oczekujesz zadania, które zostało zwrócone. – GolezTrol

0
var stream = await file.readAsStreamAsync(); 
DoStuff(stream); 

jest koncepcyjnie raczej

file.readAsStreamAsync(stream => { 
    DoStuff(stream); 
}); 

gdzie lambda jest automatycznie wywoływana, gdy strumień został w pełni odczytać. Możesz zobaczyć, że to coś zupełnie innego niż kod blokujący.

Jeśli budowania aplikacji UI dla przykładu i wdrożenie obsługi przycisku:

private async void HandleClick(object sender, EventArgs e) 
{ 
    ShowProgressIndicator(); 

    var response = await GetStuffFromTheWebAsync(); 
    DoStuff(response); 

    HideProgressIndicator(); 
} 

To drastycznie różni się od podobnego kodu synchronicznego:

private void HandleClick(object sender, EventArgs e) 
{ 
    ShowProgressIndicator(); 

    var response = GetStuffFromTheWeb(); 
    DoStuff(response); 

    HideProgressIndicator(); 
} 

ponieważ w drugi kod UI zostanie zablokowany i nigdy nie zobaczysz wskaźnika postępu (lub w najlepszym razie będzie on migał krótko), ponieważ wątek UI zostanie zablokowany, dopóki cała obsługa kliknięcia nie zostanie zakończona. W pierwszym kodzie widoczny jest wskaźnik postępu, a następnie wątek interfejsu użytkownika ponownie uruchamia się, gdy wywołanie internetowe odbywa się w tle, a następnie, gdy wywołanie internetowe zakończy kod DoStuff(response); HideProgressIndicator();, zostanie zaplanowane w wątku UI, a ładnie zakończy swoją pracę i ukryje wskaźnik postępu.

0

Wygląda na to, że brakuje ci tego wszystkiego, o czym jest ta cała koncepcja async/await.

Słowo kluczowe async Niech kompilator wie, że metoda może wymagać wykonania niektórych operacji asynchronicznych i dlatego nie powinna być wykonywana w normalny sposób, jak każda inna metoda, zamiast tego powinna być traktowana jako automat stanów. Oznacza to, że najpierw kompilator wykona tylko część metody (nazwijmy ją Part 1), a następnie uruchom jakieś operacje asynchroniczne na innym wątku zwalniającym wątek wywołujący. Kompilator również zaplanuje część 2 do wykonania pierwszego dostępnego wątku z ThreadPool. Jeśli operacja asynchroniczna nie jest oznaczona słowem kluczowym await, to nie jest oczekiwana, a wątek wywołujący będzie działał, dopóki metoda nie zostanie zakończona. W większości przypadków nie jest to pożądane. Wtedy potrzebujemy użyć słowa kluczowego await.

więc typowy scenariusz:

gwintu 1 wchodzi metody asynchronicznej i wykonuje kod Part1 ->

Temat 1 rozpoczyna asynchronicznych operacji ->

gwintu 1 jest zwolniony, operacja trwa Part2 zaplanowano w TP ->

Niektóre wątek (najprawdopodobniej samym wątku 1 jest jego wolny) kontynuuje działanie metody do jej końca (Part2) ->

1

Znacznie ułatwia pisanie kodu asynchronicznego . Jak zauważyłeś w swoim własnym pytaniu, to wygląda tak, jakbyś pisał wariant synchroniczny - ale w rzeczywistości jest on asynchroniczny.

Aby to zrozumieć, trzeba naprawdę wiedzieć, co znaczy asynchroniczny i synchroniczny. Znaczenie jest naprawdę proste - synchroniczne oznacza w sekwencji, jedna po drugiej. Asynchroniczne oznacza poza kolejnością. Ale to nie jest cały obraz tutaj - te dwa słowa są prawie bezużyteczne same w sobie, większość ich znaczenia pochodzi z kontekstu. Musisz zapytać: synchroniczne w odniesieniu do , co dokładnie?

Załóżmy, że masz aplikację WinForm, która musi odczytać plik. W przycisku klikamy, robimy File.ReadAllText i umieszczamy wyniki w jakimś polu tekstowym - wszystko w porządku i dandy. Operacja We/Wy jest synchroniczna w odniesieniu do Twojego interfejsu użytkownika - interfejs użytkownika nie może nic zrobić, gdy czekasz na zakończenie operacji wejścia/wyjścia. Teraz klienci zaczynają narzekać, że interfejs użytkownika wydaje się zawieszony na kilka sekund w czasie, gdy czyta plik - a Windows oznacza zgłoszenie jako "nie odpowiada". Dlatego decydujesz się przekazać odczyt pliku do pracownika działającego w tle - na przykład, używając BackgroundWorker lub Thread. Teraz twoja operacja I/O jest asynchroniczna w odniesieniu do twojego interfejsu użytkownika i wszyscy są szczęśliwi - wystarczy, że wydasz swoją pracę i uruchomisz ją we własnym wątku, yay.

Teraz jest to naprawdę w porządku - o ile tylko wykonujesz jedną taką operację asynchroniczną jednocześnie. Jednakże oznacza to, że musisz jednoznacznie określić, gdzie granice wątku interfejsu użytkownika - musisz odpowiednio zsynchronizować. Oczywiście, w WinForm jest to dość proste, ponieważ możesz po prostu użyć Invoke do przywrócenia pracy UI z powrotem do wątku UI - ale co jeśli potrzebujesz interakcji z interfejsem wielokrotnie podczas pracy w tle? Jasne, jeśli chcesz tylko publikować wyniki w sposób ciągły, nie ma problemu z BackgroundWorker s ReportProgress - ale co, jeśli chcesz również obsługiwać dane wprowadzane przez użytkownika?

Piękno await jest to, że można łatwo zarządzać, gdy jesteś w wątku tła, a kiedy jesteś w kontekście synchronizacji (takich jak Windows Forms wątku UI):

string line; 
while ((line = await streamReader.ReadLineAsync()) != null) 
{ 
    if (line.StartsWith("ERROR:")) tbxLog.AppendLine(line); 
    if (line.StartsWith("CRITICAL:")) 
    { 
    if (MessageBox.Show(line + "\r\n" + "Do you want to continue?", 
         "Critical error", MessageBoxButtons.YesNo) == DialogResult.No) 
    { 
     return; 
    } 
    } 

    await httpClient.PostAsync(...); 
} 

ten jest cudowny - zasadniczo piszesz kod synchroniczny jak zwykle, ale nadal jest on asynchroniczny w stosunku do wątku UI. Obsługa błędów jest znowu dokładnie taka sama jak w przypadku każdego kodu synchronicznego - using, try-finally, a wszyscy znajomi działają świetnie.

Okej, więc nie musisz zraszać tu i tam BeginInvoke, o co chodzi? Naprawdę wielką sprawą jest to, że bez żadnego wysiłku z twojej strony, faktycznie zacząłeś używać prawdziwych asynchronicznych API dla wszystkich tych operacji we/wy. Chodzi o to, że tak naprawdę nie ma żadnych synchronicznych operacji we/wy w systemie operacyjnym - kiedy robisz to "synchronicznie" File.ReadAllText, system operacyjny po prostu wysyła asynchroniczne żądanie we/wy, a następnie blokuje wątek do momentu odpowiedź wraca. Oczywiste jest, że wątek jest marnowany, nie robiąc nic w międzyczasie - nadal używa zasobów systemowych, dodaje niewielką ilość pracy do harmonogramu itp.

Ponownie, w typowej aplikacji klienckiej, nie jest to Wielka rzecz. Użytkownik nie dba o to, czy masz jeden lub dwa wątki - różnica nie jest tak duża. Serwery są jednak zupełnie inną bestią; gdy typowy klient ma tylko jedną lub dwie operacje wejścia/wyjścia w tym samym czasie, serwer ma obsłużyć tysiące! W typowym 32-bitowym systemie można było zmieścić w procesie około 2000 wątków z domyślnym pakietem - nie ze względu na wymagania pamięci fizycznej, ale po prostu przez wyczerpanie wirtualnej przestrzeni adresowej. Procesy 64-bitowe nie są tak ograniczone, ale wciąż jest coś, co uruchamia nowe wątki i je niszczy jest dość drogie, a ty teraz dodajesz znaczną pracę do harmonogramu wątków systemu operacyjnego - tylko po to, aby te wątki czekały.

Jednak kod oparty na await nie ma tego problemu. Zajmuje tylko wątek podczas pracy z procesorem - oczekiwanie na operację I/O, aby zakończyć, to praca procesora , a nie. Wystawiasz więc to asynchroniczne żądanie we/wy, a twój wątek powraca do puli wątków. Po otrzymaniu odpowiedzi inny wątek jest pobierany z puli wątków. Nagle, zamiast używać tysięcy wątków, twój serwer używa tylko pary (zwykle około dwóch na rdzeń procesora). Wymagania dotyczące pamięci są mniejsze, znacznie zmniejszają się koszty związane z wielowątkowością, a całkowita przepustowość znacznie wzrasta.

Tak więc, w aplikacji klienckiej, await jest tylko kwestią wygody. W każdej większej aplikacji na serwerze jest to konieczność - ponieważ nagle twoje podejście "rozpocznij nowy wątek" po prostu się nie skaluje. Alternatywą dla używania await są wszystkie te oldschoolowe asynchroniczne interfejsy API, które obsługują jak kod synchroniczny, a błędy obsługi są bardzo uciążliwe i trudne.