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.
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. –
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
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