Jaka jest różnica między programowaniem asynchronicznym a wielowątkowością?

234

Myślałem, że to w zasadzie to samo - pisanie programów, które dzielą zadania między procesory (na komputerach z procesorami 2+). Następnie czytam to , co mówi:

Metody asynchroniczne mają być operacjami nieblokującymi. Wyrażenie oczekujące w metodzie asynchronicznej nie blokuje bieżącego wątku, gdy oczekiwane zadanie jest uruchomione. Zamiast tego wyrażenie podpisuje resztę metody jako kontynuację i zwraca kontrolę do obiektu wywołującego metodę asynchroniczną.

Słowa kluczowe asynchroniczne i oczekujące nie powodują utworzenia dodatkowych wątków. Metody asynchroniczne nie wymagają wielowątkowości, ponieważ metoda asynchroniczna nie działa na własnym wątku. Metoda działa w bieżącym kontekście synchronizacji i wykorzystuje czas w wątku tylko wtedy, gdy metoda jest aktywna. Możesz użyć Task.Run, aby przenieść pracę związaną z CPU do wątku w tle, ale wątek w tle nie pomaga w procesie, który tylko czeka na wyniki.

i zastanawiam się, czy ktoś może dla mnie przetłumaczyć to na angielski. Wydaje się, że wprowadza rozróżnienie między asynchronicznością (czy to słowo?) A wątkami i sugeruje, że możesz mieć program, który ma zadania asynchroniczne, ale nie ma wielowątkowości.

Teraz rozumiem ideę zadań asynchronicznych, takich jak przykład na pg. 467 Jona Skeeta C # In Depth, wydanie trzecie

async void DisplayWebsiteLength ( object sender, EventArgs e )
{
    label.Text = "Fetching ...";
    using ( HttpClient client = new HttpClient() )
    {
        Task<string> task = client.GetStringAsync("http://csharpindepth.com");
        string text = await task;
        label.Text = text.Length.ToString();
    }
}

Słowo asynckluczowe oznacza „ Ta funkcja, ilekroć jest wywoływana, nie będzie wywoływana w kontekście, w którym jej zakończenie jest wymagane do wywołania wszystkiego po wywołaniu”.

Innymi słowy, pisanie go w trakcie jakiegoś zadania

int x = 5; 
DisplayWebsiteLength();
double y = Math.Pow((double)x,2000.0);

, Ponieważ DisplayWebsiteLength()nie ma nic wspólnego z xlub yspowoduje DisplayWebsiteLength()być realizowane „w tle”, jak

                processor 1                |      processor 2
-------------------------------------------------------------------
int x = 5;                                 |  DisplayWebsiteLength()
double y = Math.Pow((double)x,2000.0);     |

Oczywiście to głupi przykład, ale czy mam rację, czy jestem całkowicie zdezorientowany, czy co?

(Poza tym jestem zdezorientowany, dlaczego senderi enigdy nie są używane w treści powyższej funkcji.)

użytkownik5648283
źródło
senderi esugerują, że jest to w rzeczywistości moduł obsługi zdarzeń - właściwie jedyne miejsce, w którym async voidjest to pożądane. Najprawdopodobniej jest to wywoływane kliknięciem przycisku lub czymś podobnym - w wyniku tego działanie to dzieje się całkowicie asynchronicznie w stosunku do reszty aplikacji. Ale wciąż jest w jednym wątku - wątku interfejsu użytkownika (z niewielkim odcinkiem czasu w wątku IOCP, który wysyła wywołanie zwrotne do wątku interfejsu użytkownika).
Luaan,
Możliwy duplikat różnicy między wielowątkowością a programem
asynchronicznym
3
Bardzo ważna uwaga na DisplayWebsiteLengthprzykładowym kodzie: Nie powinieneś używać HttpClientw usinginstrukcji - Pod dużym obciążeniem kod może wyczerpać dostępną liczbę gniazd, co powoduje błędy SocketException. Więcej informacji na temat niewłaściwej instancji .
Gan
1
@JakubLortz Nie wiem, dla kogo jest ten artykuł. Nie dla początkujących, ponieważ wymaga dobrej wiedzy na temat wątków, przerwań, rzeczy związanych z procesorem itp. Nie dla zaawansowanych użytkowników, ponieważ dla nich wszystko jest już jasne. Jestem pewien, że nie pomoże to nikomu zrozumieć, o co w tym wszystkim chodzi - zbyt wysoki poziom abstrakcji.
Loreno

Odpowiedzi:

589

Twoje nieporozumienie jest niezwykle powszechne. Wiele osób uczy się, że wielowątkowość i asynchronia są tym samym, ale tak nie jest.

Analogia zwykle pomaga. Gotujesz w restauracji. Nadchodzi zamówienie na jajka i tosty.

  • Synchroniczny: gotujesz jajka, a następnie gotujesz tosty.
  • Asynchroniczne, jednowątkowe: zaczynasz gotować jajka i ustawiasz czas. Rozpocznij gotowanie toastowe i ustaw zegar. Kiedy oboje gotują, myjecie kuchnię. Kiedy zegary się wyłączają, zdejmujesz jajka z ognia i tosty z tostera i podajesz je.
  • Asynchroniczny, wielowątkowy: zatrudniasz dwóch dodatkowych kucharzy, jednego do gotowania jajek i jednego do gotowania tostów. Teraz masz problem z koordynacją kucharzy, aby nie kolidowali ze sobą w kuchni podczas dzielenia się zasobami. I musisz je zapłacić.

Czy to ma sens, że wielowątkowość to tylko jeden rodzaj asynchronii? Wątki dotyczą pracowników; asynchronia dotyczy zadań . W wielowątkowych przepływach pracy przypisujesz zadania pracownikom. W asynchronicznych jedno-wątkowych przepływach pracy masz wykres zadań, w którym niektóre zadania zależą od wyników innych; po zakończeniu każdego zadania wywołuje kod, który planuje następne zadanie, które można uruchomić, biorąc pod uwagę wyniki właśnie ukończonego zadania. Ale (miejmy nadzieję) potrzebujesz tylko jednego pracownika do wykonania wszystkich zadań, a nie jednego pracownika na zadanie.

Pomoże to zrozumieć, że wiele zadań nie jest związanych z procesorem. W przypadku zadań związanych z procesorem sensowne jest zatrudnienie tylu pracowników (wątków), ile jest procesorów, przypisanie jednego zadania do każdego pracownika, przypisanie jednego procesora do każdego pracownika i zlecenie każdemu procesorowi wykonywania innych zadań, jak tylko obliczenie wyniku jako tak szybko jak to możliwe. Ale w przypadku zadań, które nie oczekują na procesorze, nie trzeba wcale przypisywać pracownika. Po prostu czekasz na wiadomość, że wynik jest dostępny, i robisz coś innego, czekając . Gdy ta wiadomość dotrze, możesz zaplanować kontynuację ukończonego zadania jako kolejną rzecz na liście spraw do sprawdzenia.

Spójrzmy więc bardziej szczegółowo na przykład Jona. Co się dzieje?

  • Ktoś wywołuje DisplayWebSiteLength. WHO? Nie obchodzi nas to.
  • Ustawia etykietę, tworzy klienta i prosi klienta o pobranie czegoś. Klient zwraca obiekt reprezentujący zadanie pobrania czegoś. To zadanie jest w toku.
  • Czy trwa w innym wątku? Prawdopodobnie nie. Przeczytaj artykuł Stephena, dlaczego nie ma wątku.
  • Teraz czekamy na zadanie. Co się dzieje? Sprawdzamy, czy zadanie zakończyło się między momentem, w którym go utworzyliśmy, a my na niego czekaliśmy. Jeśli tak, pobieramy wynik i kontynuujemy działanie. Załóżmy, że nie został ukończony. Pozostałą część tej metody rejestrujemy jako kontynuację tego zadania i powrót .
  • Teraz kontrola powróciła do dzwoniącego. Co to robi? Cokolwiek chce.
  • Załóżmy teraz, że zadanie zostało zakończone. Jak to zrobił? Może działał w innym wątku, a może obiekt wywołujący, do którego właśnie wróciliśmy, pozwolił mu uruchomić się w bieżącym wątku. Niezależnie od tego mamy teraz ukończone zadanie.
  • Ukończone zadanie prosi właściwy wątek - ponownie, prawdopodobnie jedyny wątek - o uruchomienie kontynuacji zadania.
  • Kontrola przechodzi natychmiast z powrotem do metody, którą właśnie opuściliśmy w punkcie oczekiwania. Teraz dostępny jest wynik, dzięki czemu możemy przypisać texti uruchomić resztę metody.

To tak jak w mojej analogii. Ktoś prosi o dokument. Wysyłasz dokument pocztą i kontynuujesz wykonywanie innych prac. Kiedy nadejdzie na pocztę, zostaniesz zasygnalizowany, a gdy masz na to ochotę, wykonasz resztę przepływu pracy - otwórz kopertę, opłać opłaty za dostawę, cokolwiek. Nie musisz zatrudniać innego pracownika, aby zrobić to wszystko za Ciebie.

Eric Lippert
źródło
8
@ user5648283: Sprzęt jest nieodpowiedni do myślenia o zadaniach. Zadanie to po prostu obiekt, który (1) oznacza, że ​​wartość stanie się dostępna w przyszłości i (2) może uruchomić kod (w odpowiednim wątku), gdy ta wartość będzie dostępna . To, jak każde indywidualne zadanie uzyska wynik w przyszłości, zależy od niego. Niektórzy używają do tego specjalnego sprzętu, takiego jak „dyski” i „karty sieciowe”; niektórzy używają sprzętu takiego jak procesory.
Eric Lippert,
13
@ user5648283: Ponownie pomyśl o mojej analogii. Gdy ktoś prosi o ugotowanie jajek i tostów, używasz specjalnego sprzętu - kuchenki i tostera - i możesz wyczyścić kuchnię, gdy sprzęt wykonuje swoją pracę. Jeśli ktoś poprosi cię o jajka, tosty i oryginalną krytykę ostatniego filmu Hobbita, możesz napisać swoją recenzję podczas gotowania jajek i tostów, ale nie musisz do tego używać sprzętu.
Eric Lippert,
9
@ user5648283: Zastanów się teraz nad pytaniem dotyczącym „zmiany układu kodu”. Załóżmy, że masz metodę P, która ma zwrot z zysku, i metodę Q, która wykonuje foreach nad wynikiem P. Krok przez kod. Zobaczysz, że wykonujemy trochę Q, potem P, a potem Q ... Rozumiesz o co chodzi? czekają zasadniczo na zwrot z inwestycji w fantazyjnej sukience . Czy to jest teraz bardziej jasne?
Eric Lippert
10
Toster to sprzęt. Sprzęt nie potrzebuje nic do obsługi; dyski i karty sieciowe i co tam działają na poziomie znacznie poniżej poziomu wątków systemu operacyjnego.
Eric Lippert,
5
@ShivprasadKoirala: To absolutnie nie jest prawda . Jeśli w to wierzysz, masz bardzo fałszywe przekonania na temat asynchronii . Cały sens asynchronii w języku C # polega na tym, że nie tworzy on wątku.
Eric Lippert,
27

JavaScript w przeglądarce jest doskonałym przykładem programu asynchronicznego, który nie ma wątków.

Nie musisz się martwić, że wiele fragmentów kodu dotyka jednocześnie tych samych obiektów: każda funkcja zakończy działanie, zanim jakikolwiek inny skrypt javascript zostanie dopuszczony do działania na stronie.

Jednak podczas wykonywania czegoś takiego jak żądanie AJAX żaden kod nie jest w ogóle uruchomiony, więc inny skrypt javascript może reagować na takie zdarzenia, jak kliknięcia, dopóki żądanie nie wróci i nie wywoła powiązanego z nim wywołania zwrotnego. Jeśli jeden z tych innych programów obsługi zdarzeń nadal działa, gdy żądanie AJAX powróci, jego moduł obsługi nie zostanie wywołany, dopóki nie zostanie zakończony. Działa tylko jeden „wątek” JavaScript, mimo że możliwe jest skuteczne wstrzymanie wykonywanej czynności do momentu uzyskania potrzebnych informacji.

W aplikacjach C # to samo dzieje się za każdym razem, gdy masz do czynienia z elementami interfejsu użytkownika - możesz wchodzić w interakcje z elementami interfejsu użytkownika tylko w wątku interfejsu użytkownika. Jeśli użytkownik kliknął przycisk, a użytkownik chciał odpowiedzieć czytając duży plik z dysku, niedoświadczony programista może popełnić błąd odczytu pliku w samej procedurze obsługi zdarzenia kliknięcia, co spowodowałoby „zawieszenie się” aplikacji do momentu plik zakończył ładowanie, ponieważ nie można reagować na żadne kliknięcia, najechanie kursorem ani inne zdarzenia związane z interfejsem użytkownika, dopóki ten wątek nie zostanie zwolniony.

Jedną z opcji, której programiści mogą użyć, aby uniknąć tego problemu, jest utworzenie nowego wątku w celu załadowania pliku, a następnie poinformowanie kodu tego wątku, że po załadowaniu pliku musi ponownie uruchomić pozostały kod w wątku interfejsu użytkownika, aby mógł zaktualizować elementy interfejsu użytkownika na podstawie tego, co znalazł w pliku. Do niedawna takie podejście było bardzo popularne, ponieważ ułatwiało to biblioteki i język C #, ale jest zasadniczo bardziej skomplikowane, niż musi być.

Jeśli myślisz o tym, co robi procesor, gdy czyta plik na poziomie sprzętu i systemu operacyjnego, to w zasadzie wydaje polecenie odczytania fragmentów danych z dysku do pamięci i uderzenia w system operacyjny z „przerwaniem” „po zakończeniu odczytu. Innymi słowy, czytanie z dysku (lub dowolnego wejścia / wyjścia naprawdę) jest z natury asynchroniczną operacją. Koncepcja wątku oczekującego na zakończenie operacji we / wy jest abstrakcją, którą twórcy bibliotek stworzyli, aby ułatwić programowanie. To nie jest konieczne.

Teraz większość operacji we / wy w .NET ma odpowiednią ...Async()metodę, którą można wywołać, która zwraca Taskprawie natychmiast. Możesz do tego dodać wywołania zwrotne, aby Taskokreślić kod, który chcesz uruchomić po zakończeniu operacji asynchronicznej. Możesz także określić, w którym wątku chcesz uruchomić ten kod, i możesz podać token, który operacja asynchroniczna może od czasu do czasu sprawdzać, aby sprawdzić, czy zdecydowano się anulować zadanie asynchroniczne, co daje mu możliwość szybkiego zatrzymania pracy i z wdziękiem.

Do czasu async/awaitdodania słów kluczowych C # był znacznie bardziej oczywisty na temat sposobu wywoływania kodu wywołania zwrotnego, ponieważ te wywołania zwrotne były w formie delegatów powiązanych z zadaniem. Aby nadal korzystać z ...Async()operacji, unikając złożoności kodu, async/awaitabstrahuje od tworzenia tych delegatów. Ale wciąż są tam w skompilowanym kodzie.

Dzięki temu moduł obsługi zdarzeń interfejsu użytkownika może mieć awaitoperację we / wy, zwalniając wątek interfejsu użytkownika do wykonywania innych czynności i mniej więcej automatycznie wracając do wątku interfejsu użytkownika po zakończeniu odczytu pliku - bez konieczności utwórz nowy wątek.

StriplingWarrior
źródło
Działa tylko jeden „wątek” JavaScript - nie jest już prawdziwy w przypadku pracowników sieci Web .
oleksii
6
@oleksii: Technicznie jest to prawda, ale nie zamierzałem się w to zagłębiać, ponieważ sam interfejs API Web Workers jest asynchroniczny, a pracownicy Web nie mogą bezpośrednio wpływać na wartości javascript lub DOM na wywoływanej stronie from, co oznacza, że ​​drugi ważny punkt tej odpowiedzi jest nadal aktualny. Z perspektywy programisty nie ma różnicy między wywołaniem Web Workera a wywołaniem żądania AJAX.
StriplingWarrior