Jak działa jednowątkowy nieblokujący model IO w Node.js


325

Nie jestem programistą węzłów, ale interesuje mnie, jak działa jednowątkowy, nieblokujący model IO . Po przeczytaniu artykułu dotyczącego zrozumienia pętli zdarzeń węzła-js jestem naprawdę zdezorientowany. Dał przykład dla modelu:

c.query(
   'SELECT SLEEP(20);',
   function (err, results, fields) {
     if (err) {
       throw err;
     }
     res.writeHead(200, {'Content-Type': 'text/html'});
     res.end('<html><head><title>Hello</title></head><body><h1>Return from async DB query</h1></body></html>');
     c.end();
    }
);

Que: Gdy są dwa żądania A (przychodzi pierwszy) i B, ponieważ istnieje tylko jeden wątek, program po stronie serwera obsłuży żądanie A po pierwsze: wykonywanie zapytań SQL jest uśpioną instrukcją dla czekania we / wy. Program utknął w I/Ooczekiwaniu i nie może wykonać kodu renderującego stronę internetową. Czy program przełączy się na żądanie B podczas oczekiwania? Moim zdaniem, ze względu na model z jednym wątkiem, nie ma możliwości przełączenia jednego żądania z drugiego. Ale tytuł przykładowego kodu mówi, że wszystko działa równolegle oprócz twojego kodu .

(PS Nie jestem pewien, czy źle zrozumiałem kod, ponieważ nigdy nie korzystałem z Węzła.) Jak Węzeł przełącza A na B podczas oczekiwania? I czy możesz w prosty sposób wyjaśnić jednowątkowy nieblokujący model IO węzła? Byłbym wdzięczny, gdybyś mógł mi pomóc. :)

Odpowiedzi:


374

Node.js jest oparty na libuv , wieloplatformowej bibliotece, która wyodrębnia apis / syscalls dla asynchronicznych (nieblokujących) wejść / wyjść dostarczanych przez obsługiwane systemy operacyjne (przynajmniej Unix, OS X i Windows).

Asynchroniczne we / wy

W tym modelu programowania operacja otwierania / odczytu / zapisu na urządzeniach i zasobach (gniazdach, systemie plików itp.) Zarządzanych przez system plików nie blokuje wątku wywołującego (jak w typowym modelu synchronicznym typu c) i po prostu zaznacza proces (w strukturze danych na poziomie jądra / systemu operacyjnego), który ma być powiadamiany o dostępności nowych danych lub zdarzeń. W przypadku aplikacji podobnej do serwera WWW proces jest następnie odpowiedzialny za ustalenie, do którego żądania / kontekstu należy zgłoszone zdarzenie, i kontynuowanie przetwarzania żądania z tego miejsca. Zauważ, że to koniecznie będzie oznaczać, że będziesz na innej ramce stosu niż ta, która zainicjowała żądanie do systemu operacyjnego, ponieważ ten ostatni musiał ustąpić dyspozytorowi procesu, aby pojedynczy proces wątkowy obsługiwał nowe zdarzenia.

Problem z modelem, który opisałem, polega na tym, że programista nie jest zaznajomiony i trudny do uzasadnienia, ponieważ ma niesekwencyjny charakter. „Musisz złożyć żądanie w funkcji A i obsłużyć wynik w innej funkcji, w której lokalni mieszkańcy z A zwykle nie są dostępni.”

Model węzła (styl kontynuacji przekazywania i pętla zdarzeń)

Węzeł rozwiązuje ten problem, wykorzystując funkcje języka javascript, aby ten model wyglądał nieco bardziej synchronicznie, zachęcając programistę do stosowania określonego stylu programowania. Każda funkcja function (... parameters ..., callback)żądająca We / Wy ma podpis podobny do i musi otrzymać wywołanie zwrotne, które zostanie wywołane po zakończeniu żądanej operacji (pamiętaj, że większość czasu spędza się na oczekiwaniu na zakończenie przez system operacyjny - czas ten może być spędzony na wykonywaniu innej pracy). Obsługa JavaScript dla zamknięć pozwala na użycie zmiennych zdefiniowanych w funkcji zewnętrznej (wywołanie) wewnątrz treści wywołania zwrotnego - pozwala to zachować stan między różnymi funkcjami, które będą wywoływane niezależnie przez środowisko wykonawcze węzła. Zobacz także Styl przekazywania kontynuacji .

Ponadto po wywołaniu funkcji spawnującej operację We / Wy funkcja wywołująca zwykle returnkontroluje pętlę zdarzeń węzła . Ta pętla wywoła następne wywołanie zwrotne lub funkcję, która została zaplanowana do wykonania (najprawdopodobniej dlatego, że odpowiednie zdarzenie zostało powiadomione przez system operacyjny) - pozwala to na równoczesne przetwarzanie wielu żądań.

Możesz myśleć o pętli zdarzeń węzła jako nieco podobnej do dyspozytora jądra : jądro zaplanuje wykonanie zablokowanego wątku po zakończeniu oczekującego We / Wy, a węzeł zaplanuje wywołanie zwrotne po wystąpieniu odpowiedniego zdarzenia.

Wysoce zbieżny, brak równoległości

Jako ostatnia uwaga, wyrażenie „wszystko działa równolegle oprócz twojego kodu” robi porządną operację przechwytywania punktu, w którym węzeł pozwala Twojemu kodowi obsługiwać żądania z setek tysięcy otwartych gniazd za pomocą jednego wątku jednocześnie przez multipleksowanie i sekwencjonowanie wszystkich twoich plików js logika w jednym strumieniu wykonania (nawet jeśli powiedzenie „wszystko działa równolegle” prawdopodobnie tutaj nie jest poprawne - patrz Współbieżność z równoległością - Jaka jest różnica? ). Działa to całkiem dobrze w przypadku serwerów aplikacji webowych, ponieważ większość czasu faktycznie spędza się na czekaniu na sieć lub dysk (bazę danych / gniazda), a logika tak naprawdę nie wymaga dużego procesora - to znaczy: działa dobrze w przypadku obciążeń związanych z operacjami we / wy .


45
Dalsze pytania: jak to się dzieje w takim przypadku? Węzeł wysyła żądanie do systemu i prosi o powiadomienie, gdy zostanie zakończone. Czy system działa w wątku wykonującym operacje we / wy, czy też system wykonuje również operacje we / wy asynchronicznie na poziomie sprzętowym, używając przerwań? Coś gdzieś musi czekać na zakończenie operacji we / wy, a to zablokuje się, dopóki nie zostanie wykonane i zużyje pewną ilość zasobów.
Philip

6
Właśnie zauważyłem, że na ten komentarz uzupełniający odpowiada @ user568109 poniżej, chciałbym, aby istniał sposób na połączenie tych dwóch odpowiedzi.
lfalin

4
Chciałbym, żebyś napisał odpowiedź dwa razy dłużej, więc zrozumiałbym dwa razy lepiej.
Rafael Eyng

Dla przypomnienia, węzeł jest obsługiwany w wielu miejscach. Kiedy projektowałem oprogramowanie układowe dla routerów MIPS32, Node.JS można było uruchomić na tych przez OpenWRT.
Qix - MONICA MISTREATEDED

Jak ocenia się w stosunku do apache? Apache jest również w stanie obsługiwać równoczesne połączenia z oddzielnym wątkiem.
Suhail Gupta

210

Cóż, aby dać trochę perspektywy, pozwól mi porównać node.js z Apache.

Apache jest wielowątkowym serwerem HTTP, dla każdego otrzymanego żądania serwer tworzy osobny wątek, który obsługuje to żądanie.

Z drugiej strony Node.js jest sterowany zdarzeniami, obsługując wszystkie żądania asynchronicznie z jednego wątku.

Kiedy A i B są odbierane w Apache, tworzone są dwa wątki, które obsługują żądania. Każde z nich obsługuje osobno zapytanie, każde czeka na wyniki zapytania przed wyświetleniem strony. Strona jest wyświetlana tylko do momentu zakończenia zapytania. Pobieranie zapytania jest blokowane, ponieważ serwer nie może wykonać reszty wątku, dopóki nie otrzyma wyniku.

W węźle c.query jest obsługiwane asynchronicznie, co oznacza, że ​​podczas gdy c.query pobiera wyniki dla A, przeskakuje do obsługi c. Zapytania dla B, a gdy wyniki docierają do A, odsyła wyniki do wywołania zwrotnego, które wysyła odpowiedź. Node.js wie, jak wykonać wywołanie zwrotne po zakończeniu pobierania.

Moim zdaniem, ponieważ jest to model z jednym wątkiem, nie ma możliwości przełączenia się z jednego żądania na drugie.

W rzeczywistości serwer węzłów robi to dokładnie za Ciebie przez cały czas. Aby wykonać przełączniki, (zachowanie asynchroniczne) większość funkcji, które można użyć, będzie miała wywołania zwrotne.

Edytować

Zapytanie SQL jest pobierane z biblioteki mysql . Implementuje styl wywołania zwrotnego oraz emiter zdarzeń do kolejkowania żądań SQL. Nie wykonuje ich asynchronicznie, co jest wykonywane przez wewnętrzne wątki libuv, które zapewniają abstrakcję nieblokujących operacji we / wy. Aby utworzyć zapytanie, wykonaj następujące czynności:

  1. Otwórz połączenie z db, samo połączenie można wykonać asynchronicznie.
  2. Po podłączeniu bazy danych zapytanie jest przekazywane do serwera. Zapytania można umieszczać w kolejce.
  3. Główna pętla zdarzeń jest powiadamiana o zakończeniu za pomocą wywołania zwrotnego lub zdarzenia.
  4. Pętla główna wykonuje funkcję obsługi wywołania zwrotnego / zdarzenia.

Przychodzące żądania do serwera HTTP są obsługiwane w podobny sposób. Architektura wewnętrznego wątku jest mniej więcej taka:

Pętla zdarzeń node.js

Wątki C ++ są libuv, które wykonują asynchroniczne operacje we / wy (dysk lub sieć). Główna pętla zdarzeń jest nadal wykonywana po wysłaniu żądania do puli wątków. Może przyjmować więcej żądań, ponieważ nie czeka ani nie śpi. Zapytania SQL / żądania HTTP / system plików czyta wszystko dzieje się w ten sposób.


16
Schemat jest bardzo przydatny.
Anmol Saraf,

14
Poczekaj, więc na diagramie masz „wewnętrzną pulę wątków C ++”, co oznacza, że ​​wszystkie operacje blokowania IO spowodują odrodzenie się wątku, prawda? Więc jeśli moja aplikacja Node działa na każdym żądaniu , to czy nie ma praktycznie żadnej różnicy między modelem Node a modelem Apache? Nie przepraszam za tę część.
gav.newalkar

21
@ gav.newalkar Nie spawnują wątku, żądania są w kolejce. Przetwarzają je wątki w puli wątków. Wątki nie są dynamiczne i na żądanie, jak w Apache. Zazwyczaj są one stałe i różnią się w zależności od systemu.
user568109

10
@ user568109 Ale Apache również używa puli wątków ( httpd.apache.org/docs/2.4/mod/worker.html ). Ostatecznie różnica między konfiguracją z node.js różni się od konfiguracji z Apache z przodu tylko w tym miejscu, w którym znajduje się pula wątków, prawda?
Kris

13
Ten schemat powinien znajdować się na pierwszej stronie oficjalnych dokumentów.
bouvierr

52

Node.js używa libuv za kulisami. libuv ma pulę wątków (domyślnie rozmiar 4). Dlatego Node.js używa wątków do osiągnięcia współbieżności.

Jednak , Twój kod działa na jednym wątku (czyli wszystkie wywołania zwrotne funkcji node.js będzie nazywany w tym samym wątku, tzw pętli wątek lub wydarzenie-loop). Kiedy ludzie mówią, że „Node.js działa na jednym wątku”, naprawdę mówią „wywołania zwrotne Node.js działają na jednym wątku”.


1
Krótka, ale jasna odpowiedź (y)
Sudhanshu Gaur

1
dobra odpowiedź Chciałbym dodać, że operacje wejścia / wyjścia zdarzają się poza główną pętlą zdarzeń, wątkiem pętli i wątkiem żądania
Ionut Popa

oto odpowiedź, której szukałem od 2 godzin, w jaki sposób zarządzano współbieżnością w aplikacji jednowątkowej
Muhammad Ramzan

tak, trudno uzyskać odpowiedź „następnego poziomu”. To wyjaśnia, gdzie IO faktycznie się kończy (w puli wątków gdzie indziej)
Oliver Shaw

9

Node.js jest oparty na modelu programowania pętli zdarzeń. Pętla zdarzeń działa w jednym wątku i wielokrotnie czeka na zdarzenia, a następnie uruchamia wszystkie procedury obsługi zdarzeń subskrybowane do tych zdarzeń. Zdarzenia mogą być na przykład

  • oczekiwanie timera zostało zakończone
  • następna porcja danych jest gotowa do zapisania w tym pliku
  • nadchodzi nowe żądanie HTTP

Wszystko to działa w jednym wątku i żaden kod JavaScript nigdy nie jest wykonywany równolegle. Tak długo, jak te programy obsługi zdarzeń są małe i same czekają na kolejne zdarzenia, wszystko działa dobrze. Umożliwia to jednoczesne przetwarzanie wielu żądań przez pojedynczy proces Node.js.

(Jest trochę magii pod maską, skąd pochodzą zdarzenia. Niektóre z nich obejmują równolegle wątki robocze niskiego poziomu).

W tym przypadku SQL dzieje się wiele rzeczy (zdarzeń) między tworzeniem zapytania do bazy danych a uzyskiwaniem jego wyników w wywołaniu zwrotnym . W tym czasie pętla zdarzeń pompuje życie do aplikacji i przesyła kolejne żądania o jedno małe zdarzenie naraz. Dlatego wiele żądań jest obsługiwanych jednocześnie.

widok wysokiego poziomu pętli zdarzeń

Według: „Pętla zdarzeń od 10 000 stóp - podstawowa koncepcja za Node.js” .


5

Funkcja c.query () ma dwa argumenty

c.query("Fetch Data", "Post-Processing of Data")

Operacja „Pobierz dane” w tym przypadku jest zapytaniem DB, teraz Node.js może to obsłużyć, odradzając wątek roboczy i powierzając mu zadanie wykonania zapytania DB. (Pamiętaj, że Node.js może wewnętrznie tworzyć wątki). Umożliwia to natychmiastowy powrót funkcji bez opóźnienia

Drugi argument „Post-Processing danych” to funkcja zwrotna, struktura węzłów rejestruje to wywołanie zwrotne i jest wywoływana przez pętlę zdarzeń.

Tak więc instrukcja c.query (paramenter1, parameter2)zwróci się natychmiast, umożliwiając węzłowi zaspokojenie kolejnego żądania.

PS: Właśnie zacząłem rozumieć węzeł, tak naprawdę chciałem napisać to jako komentarz do @Philip, ale ponieważ nie miałem wystarczającej liczby punktów reputacji, napisałem to jako odpowiedź.


3

jeśli przeczytasz trochę dalej - „Oczywiście na backendie są wątki i procesy dostępu do bazy danych i wykonywania procesów. Nie są one jednak jawnie narażone na Twój kod, więc nie możesz się o nie martwić inaczej niż wiedząc że interakcje we / wy, np. z bazą danych lub innymi procesami, będą asynchroniczne z perspektywy każdego żądania, ponieważ wyniki z tych wątków są zwracane przez pętlę zdarzeń do kodu. ”

about - „wszystko działa równolegle oprócz kodu” - kod jest wykonywany synchronicznie, za każdym razem, gdy wywołujesz operację asynchroniczną, taką jak oczekiwanie na We / Wy, pętla zdarzeń obsługuje wszystko i wywołuje wywołanie zwrotne. to po prostu nie jest coś, o czym musisz pomyśleć.

w twoim przykładzie: istnieją dwa żądania A (jest pierwsze) i B. wykonujesz żądanie A, kod nadal działa synchronicznie i wykonuje żądanie B. pętla zdarzeń obsługuje żądanie A, gdy zakończy się, wywołuje oddzwonienie żądania A za pomocą wynik to samo dotyczy żądania B.


3
„Oczywiście na zapleczu znajdują się wątki i procesy dostępu do bazy danych i wykonywania procesów. Nie są one jednak jawnie narażone na kod” - jeśli zacznę od tego wyrażenia, nie widzę żadnej różnicy między tym, co Węzeł do lub dowolny framework wielowątkowy - powiedzmy Spring Framework Java - robi. Istnieją wątki, ale nie kontrolujesz ich tworzenia.
Rafael Eyng

@RafaelEyng Myślę, że do obsługi serii wielu żądań węzeł zawsze będzie miał do tego jeden wątek. Nie jestem pewien, czy każde wywołanie zwrotne jest umieszczane na nowej instancji wątków oprócz innych procesów, takich jak dostęp do bazy danych, ale przynajmniej na pewno wiemy, że węzeł nie tworzy instancji wątków za każdym razem, gdy odbierze żądanie, które będzie musiało poczekać w kolejce przed przetworzeniem (wykonanie przed oddzwanianie).
Cold Cerberus,

1

Okay, jak dotąd większość rzeczy powinna być jasna ... najtrudniejszą częścią jest SQL : jeśli w rzeczywistości nie działa w innym wątku lub procesie w całości, wykonanie SQL musi być podzielone na poszczególne kroki (za pomocą Procesor SQL stworzony do wykonywania asynchronicznego!), W którym wykonywane są te nieblokujące, a te blokujące (np. Uśpienie) mogą być faktycznie przeniesione do jądra (jako przerwanie / zdarzenie alarmowe) i umieszczone na liście zdarzeń dla główna pętla.

Oznacza to, że np. Interpretacja SQL itp. Jest wykonywana natychmiast, ale podczas oczekiwania (przechowywane jako zdarzenie, które ma przyjść w przyszłości przez jądro w jakiejś strukturze kqueue, epoll, ...; wraz z innymi operacjami IO ) główna pętla może robić inne rzeczy i ostatecznie sprawdzić, czy coś się stało z tymi IO i czeka.

Tak więc, aby sformułować to jeszcze raz: program nigdy nie jest (może się utknąć), wywołania uśpione nigdy nie są wykonywane. Ich zadaniem jest jądro (napisz coś, poczekaj, aż coś przejdzie przez sieć, czekając na upływ czasu) lub inny wątek lub proces. - Proces Węzła sprawdza, czy przynajmniej jeden z tych obowiązków jest wykonywany przez jądro w jedynym wywołaniu blokującym system operacyjny raz w każdym cyklu pętli zdarzeń. Ten punkt zostaje osiągnięty, kiedy wszystko, co nie jest blokowane, zostanie zrobione.

Jasny? :-)

Nie znam Node. Ale skąd pochodzi zapytanie?


kqueue epoll służy do skalowalnego asynchronicznego powiadomienia we / wy w jądrze systemu Linux. Węzeł ma do tego libuv. Węzeł znajduje się w całości na obszarze użytkownika. Nie zależy to od tego, co implementuje jądro.
user568109,

1
@ user568109, libuv jest pośrednikiem Node. Każda struktura asynchroniczna zależy (bezpośrednio lub nie) od pewnej asynchronicznej obsługi we / wy w jądrze. Więc?
Robert Siemer,

Przepraszam za zamieszanie. Operacje na gniazdach wymagają nieblokującego wejścia / wyjścia z jądra. Dba o obsługę asynchroniczną. Ale asynchroniczne operacje wejścia / wyjścia są obsługiwane przez sam libuv. Twoja odpowiedź tego nie mówi. Oba są traktowane tak samo, ponieważ są obsługiwane przez jądro.
user568109,
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.