Kiedy jest używana pula wątków?

104

Więc rozumiem, jak działa Node.js: ma jeden wątek nasłuchiwania, który odbiera zdarzenie, a następnie deleguje je do puli procesów roboczych. Wątek roboczy powiadamia odbiorcę po zakończeniu pracy, a odbiorca zwraca następnie odpowiedź do wywołującego.

Moje pytanie jest takie: jeśli włączę serwer HTTP w Node.js i wywołam uśpienie na jednym z moich zdarzeń trasowych (takich jak „/ test / sleep”), cały system się zatrzymuje. Nawet pojedynczy wątek słuchacza. Ale zrozumiałem, że ten kod dzieje się na puli pracowników.

Z drugiej strony, kiedy używam Mongoose do komunikacji z MongoDB, odczyty DB są kosztowną operacją we / wy. Wydaje się, że węzeł może delegować pracę do wątku i odbierać wywołanie zwrotne po jego zakończeniu; wydaje się, że czas potrzebny na załadowanie bazy danych nie blokuje systemu.

W jaki sposób Node.js decyduje się na użycie wątku puli wątków w porównaniu z wątkiem nasłuchiwania? Dlaczego nie mogę napisać kodu zdarzenia, który jest uśpiony i blokuje tylko wątek puli wątków?

Haney
źródło
@Tobi - widziałem to. Nadal nie odpowiada na moje pytanie. Gdyby praca była w innym wątku, sen miałby wpływ tylko na ten wątek, a nie na słuchacza.
Haney
8
Prawdziwe pytanie, w którym samemu próbujesz coś zrozumieć, a kiedy nie możesz znaleźć wyjścia z labiryntu, prosisz o pomoc.
Rafael Eyng

Odpowiedzi:

242

Twoje zrozumienie, jak działa węzeł, nie jest poprawne ... ale jest to powszechne nieporozumienie, ponieważ rzeczywistość sytuacji jest w rzeczywistości dość złożona i zazwyczaj sprowadza się do zwięzłych fraz, takich jak „węzeł jest jednowątkowy”, które nadmiernie upraszczają sprawę .

Na razie zignorujemy jawne przetwarzanie wieloprocesowe / wielowątkowe za pośrednictwem klastrów i wątków dla pracowników sieci , a po prostu porozmawiamy o typowym węźle bez wątku.

Węzeł działa w pojedynczej pętli zdarzeń. Jest jednowątkowy, a dostajesz tylko ten jeden wątek. Cały skrypt javascript, który piszesz, jest wykonywany w tej pętli, a jeśli w tym kodzie nastąpi operacja blokowania, zablokuje całą pętlę i nic więcej się nie wydarzy, dopóki się nie zakończy. Jest to typowa jednowątkowa natura węzła, o której tyle się słyszy. Ale to nie jest cały obraz.

Niektóre funkcje i moduły, zwykle napisane w C / C ++, obsługują asynchroniczne operacje we / wy. Gdy wywołujesz te funkcje i metody, wewnętrznie zarządzają przekazywaniem wywołania do wątku roboczego. Na przykład, kiedy używasz fsmodułu do żądania pliku, fsmoduł przekazuje to wywołanie do wątku roboczego, a ten pracownik czeka na odpowiedź, którą następnie przedstawia z powrotem do pętli zdarzeń, która była uruchamiana bez niego w w międzyczasie. Wszystko to jest odciągane od Ciebie, programisty węzła, a niektóre z nich są oddzielane od twórców modułów za pomocą libuv .

Jak zauważył Denis Dollfus w komentarzach (z tej odpowiedzi na podobne pytanie), strategia używana przez libuv do osiągnięcia asynchronicznych operacji we / wy nie zawsze jest pulą wątków, szczególnie w przypadku httpmodułu wydaje się być inna strategia. używane w tym czasie. Dla naszych celów ważne jest tutaj przede wszystkim zwrócenie uwagi na to, jak osiągany jest kontekst asynchroniczny (przy użyciu libuv) i że pula wątków obsługiwana przez libuv jest jedną z wielu strategii oferowanych przez tę bibliotekę w celu osiągnięcia asynchroniczności.


Jeśli chodzi o głównie pokrewną styczną, w tym doskonałym artykule znajduje się znacznie głębsza analiza tego, jak węzeł osiąga asynchroniczność, a także niektóre powiązane potencjalne problemy i sposoby ich rozwiązywania . Większość z nich rozwija to, co napisałem powyżej, ale dodatkowo zwraca uwagę:

  • Każdy moduł zewnętrzny, który umieścisz w swoim projekcie, który korzysta z natywnego C ++ i libuv, prawdopodobnie użyje puli wątków (pomyśl: dostęp do bazy danych)
  • libuv ma domyślny rozmiar puli wątków 4 i używa kolejki do zarządzania dostępem do puli wątków - rezultatem jest to, że jeśli masz 5 długo działających zapytań bazy danych, które są wykonywane w tym samym czasie, jedno z nich (i inne asynchroniczne akcja, która opiera się na puli wątków) będzie czekać na zakończenie tych zapytań, zanim jeszcze się rozpoczną
  • Możesz to złagodzić, zwiększając rozmiar puli wątków za pomocą UV_THREADPOOL_SIZEzmiennej środowiskowej, o ile robisz to, zanim pula wątków zostanie wymagana i utworzona:process.env.UV_THREADPOOL_SIZE = 10;

Jeśli chcesz tradycyjnego przetwarzania wieloprocesorowego lub wielowątkowego w węźle, możesz go uzyskać za pomocą wbudowanego clustermodułu lub różnych innych modułów, takich jak wyżej wymienione webworker-threads, lub możesz to sfałszować, wdrażając jakiś sposób dzielenia pracy i ręcznie używając setTimeoutlub setImmediatelub process.nextTickwstrzymać pracę i kontynuować ją w późniejszej pętli, aby umożliwić ukończenie innych procesów (ale nie jest to zalecane).

Pamiętaj, że jeśli piszesz długo działający / blokujący kod w javascript, prawdopodobnie popełnisz błąd. Inne języki będą działać znacznie wydajniej.

Jason
źródło
1
O cholera, to całkowicie wyjaśnia mi sprawę. Dziękuję bardzo @Jason!
Haney
5
Żaden problem :) Znalazłem się tam, gdzie nie jesteś zbyt dawno temu i ciężko było dojść do dobrze zdefiniowanej odpowiedzi, ponieważ z jednej strony masz programistów C / C ++, dla których odpowiedź jest oczywista, az drugiej masz typowe twórców stron internetowych, którzy nie zagłębiali się wcześniej w tego rodzaju pytania. Nie jestem nawet pewien, czy moja odpowiedź jest w 100% poprawna technicznie, kiedy dochodzisz do poziomu C, ale jest to właściwe w ogólnych zarysach.
Jason
3
Korzystanie z puli wątków dla żądań sieciowych byłoby ogromnym marnotrawstwem zasobów. Zgodnie z tym pytaniem „Robi asynchroniczne operacje we / wy sieci w oparciu o asynchroniczne interfejsy we / wy na różnych platformach, takich jak epoll, kqueue i IOCP, bez puli wątków” - co ma sens.
Denis Dollfus
1
... to powiedziawszy, jeśli wykonujesz ciężkie podnoszenie bezpośrednio w głównym wątku javascript, lub nie masz wystarczających zasobów lub nie zarządzasz nimi odpowiednio, aby zapewnić wystarczającą przestrzeń do puli wątków, możesz wprowadzić opóźnienie przy niższej współbieżności próg - rezultatem jest to, że dla tych samych zasobów systemowych zwykle będziesz mieć wyższą przepustowość z node.js niż z innymi opcjami (chociaż istnieją inne systemy oparte na zdarzeniach w innych językach, które mają na celu podważenie tego - nie mam widziałem jednak ostatnie testy porównawcze) - jasne jest, że model oparty na zdarzeniach przewyższa model wątkowy.
Jason
1
@Aabid Wątek nasłuchujący nie wykonuje zapytania do bazy danych, więc ukończenie wszystkich 10 z tych zapytań zajmie około 6 sekund (przy domyślnym rozmiarze puli wątków 4). Jeśli potrzebujesz wykonać jakąkolwiek pracę w javascript, która nie wymaga zakończenia tego zapytania do bazy danych, np. Przychodzi więcej żądań, które nie wymagają żadnej pracy asynchronicznej do wykonania przez pulę wątków, będzie ona nadal działać w głównym pętla zdarzeń.
Jason,
20

Więc rozumiem, jak działa Node.js: ma jeden wątek nasłuchiwania, który odbiera zdarzenie, a następnie deleguje je do puli procesów roboczych. Wątek roboczy powiadamia odbiorcę po zakończeniu pracy, a odbiorca zwraca następnie odpowiedź do wywołującego.

To nie jest do końca dokładne. Node.js ma tylko jeden wątek „roboczy”, który wykonuje wykonanie javascript. W węźle istnieją wątki, które obsługują przetwarzanie we / wy, ale myślenie o nich jako o „pracownikach” jest nieporozumieniem. Naprawdę jest tylko obsługa IO i kilka innych szczegółów dotyczących wewnętrznej implementacji węzła, ale jako programista nie możesz wpływać na ich zachowanie poza kilkoma różnymi parametrami, takimi jak MAX_LISTENERS.

Moje pytanie jest takie: jeśli włączę serwer HTTP w Node.js i wywołam uśpienie na jednym z moich zdarzeń trasowych (takich jak „/ test / sleep”), cały system się zatrzymuje. Nawet pojedynczy wątek słuchacza. Ale zrozumiałem, że ten kod dzieje się na puli pracowników.

W JavaScript nie ma mechanizmu uśpienia. Moglibyśmy omówić to bardziej konkretnie, gdybyś opublikował fragment kodu, który Twoim zdaniem oznacza „sen”. Nie ma takiej funkcji do wywołania, aby time.sleep(30)na przykład symulować coś takiego jak w Pythonie. Jest, setTimeoutale to zasadniczo NIE jest sen. setTimeouti setIntervaljawnie zwalnia , a nie blokuje pętlę zdarzeń, aby inne bity kodu mogły być wykonywane w głównym wątku wykonawczym. Jedyne, co możesz zrobić, to zapętlić procesor z obliczeniami w pamięci, co rzeczywiście spowoduje głodzenie głównego wątku wykonawczego i sprawi, że program przestanie odpowiadać.

W jaki sposób Node.js decyduje się na użycie wątku puli wątków w porównaniu z wątkiem nasłuchiwania? Dlaczego nie mogę napisać kodu zdarzenia, który jest uśpiony i blokuje tylko wątek puli wątków?

Network IO jest zawsze asynchroniczny. Koniec opowieści. Disk IO ma zarówno synchroniczne, jak i asynchroniczne interfejsy API, więc nie ma „decyzji”. node.js będzie zachowywać się zgodnie z podstawowymi funkcjami API, które wywołujesz sync w porównaniu do normalnej asynchronicznej. Na przykład: fs.readFilevs fs.readFileSync. W przypadku procesów potomnych istnieją również oddzielne child_process.execichild_process.execSync API.

Podstawową zasadą jest zawsze używanie asynchronicznych interfejsów API. Prawidłowe powody używania interfejsów API synchronizacji to kod inicjujący w usłudze sieciowej, zanim zacznie ona nasłuchiwać połączeń, lub proste skrypty, które nie akceptują żądań sieciowych dotyczących narzędzi do kompilacji i tym podobnych.

Peter Lyons
źródło
1
Skąd pochodzą te asynchroniczne interfejsy API? Rozumiem, o czym mówisz, ale ktokolwiek napisał te interfejsy API, wybrał IOCP / async. Jak zdecydowali się to zrobić?
Haney
3
Jego pytanie brzmi, jak napisałby swój własny czasochłonny kod, a nie blokował.
Jason
1
Tak. Węzeł zapewnia podstawową obsługę sieci UDP, TCP i HTTP. Zapewnia TYLKO asynchroniczne interfejsy API oparte na puli. Cały kod node.js na świecie bez wyjątku korzysta z tych asynchronicznych interfejsów API opartych na puli, ponieważ jest po prostu wszystko, co jest dostępne. System plików i procesy potomne to inna historia, ale praca w sieci jest konsekwentnie asynchroniczna.
Peter Lyons,
4
Ostrożnie, Piotrze, żebyś nie był przysłowiowym garnkiem dla jego kotła. Chce wiedzieć, jak robili to autorzy sieciowego API, a nie jak robią to ludzie używający sieciowego API. W końcu zrozumiałem, jak zachowuje się węzeł re: non-blocking events, ponieważ chciałem napisać własny nieblokujący kod, który nie ma nic wspólnego z siecią ani żadnym innym wbudowanym asynchronicznym interfejsem API. Jest całkiem jasne, że David chce zrobić to samo.
Jason
2
Węzeł nie używa pul wątków dla operacji we / wy, używa natywnych nieblokujących operacji we / wy, jedynym wyjątkiem jest fs, o ile wiem
vkurchatkin
2

Pula wątków, jak, kiedy i kto używał:

Po pierwsze, kiedy używamy / instalujemy Node na komputerze, uruchamia on proces wśród innych procesów, który nazywa się procesem węzła w komputerze i działa, dopóki go nie zabijesz. A ten proces to nasz tak zwany pojedynczy wątek.

wprowadź opis obrazu tutaj

Tak więc mechanizm pojedynczego wątku ułatwia blokowanie aplikacji węzła, ale jest to jedna z unikalnych funkcji, które Node.js wnosi do tabeli. Tak więc, ponownie, jeśli uruchomisz aplikację węzła, będzie ona działać tylko w jednym wątku. Nieważne, czy masz 1 czy milion użytkowników jednocześnie uzyskujących dostęp do Twojej aplikacji.

Zrozummy więc dokładnie, co dzieje się w pojedynczym wątku nodejs po uruchomieniu aplikacji węzła. Najpierw program jest inicjowany, następnie wykonywany jest cały kod najwyższego poziomu, co oznacza wszystkie kody, które nie znajdują się w żadnej funkcji zwrotnej ( pamiętaj, że wszystkie kody wewnątrz wszystkich funkcji zwrotnych zostaną wykonane w pętli zdarzeń ).

Następnie cały kod modułów wykonywany, a następnie rejestrują wszystkie wywołania zwrotne, w końcu pętla zdarzeń została uruchomiona dla Twojej aplikacji.

wprowadź opis obrazu tutaj

Tak więc, jak omówiliśmy wcześniej, wszystkie funkcje wywołania zwrotnego i kody wewnątrz tych funkcji będą wykonywane w pętli zdarzeń. W pętli zdarzeń obciążenia rozkładane są w różnych fazach. W każdym razie nie będę tutaj omawiał pętli zdarzeń.

Cóż, w celu lepszego zrozumienia puli wątków, proszę, abyś wyobraził sobie, że w pętli zdarzeń kody wewnątrz jednej funkcji zwrotnej są wykonywane po zakończeniu wykonywania kodów wewnątrz innej funkcji zwrotnej, teraz, jeśli są jakieś zadania, są w rzeczywistości zbyt ciężkie. Następnie zablokowaliby nasz pojedynczy wątek nodejs. I tu właśnie pojawia się pula wątków, która jest podobna do pętli zdarzeń, dostarczana do Node.js przez bibliotekę libuv.

Tak więc pula wątków nie jest częścią samego nodejs, jest dostarczana przez libuv, aby odciążyć duże obciążenia na libuv, a libuv wykona te kody we własnych wątkach i po wykonaniu libuv zwróci wyniki do zdarzenia w pętli zdarzeń.

wprowadź opis obrazu tutaj

Pula wątków daje nam cztery dodatkowe wątki, które są całkowicie oddzielone od głównego pojedynczego wątku. W rzeczywistości możemy skonfigurować do 128 wątków.

Wszystkie te wątki razem utworzyły pulę wątków. a pętla zdarzeń może następnie automatycznie przenosić ciężkie zadania do puli wątków.

Zabawne jest to, że wszystko to dzieje się automatycznie za kulisami. To nie my, programiści, decydujemy, co trafia do puli wątków, a co nie.

Do puli wątków trafia wiele zadań, takich jak

-> All operations dealing with files
->Everyting is related to cryptography, like caching passwords.
->All compression stuff
->DNS lookups
Lord
źródło
0

To nieporozumienie jest po prostu różnicą między wielozadaniowością z wywłaszczaniem a wielozadaniowością opartą na współpracy ...

Sen wyłącza cały karnawał, ponieważ do wszystkich przejażdżek jest naprawdę jedna kolejka, a ty zamknąłeś bramę. Pomyśl o tym jako o „interprecie JS i kilku innych rzeczach” i zignoruj ​​wątki ... dla ciebie jest tylko jeden wątek, ...

... więc nie blokuj tego.

Gregory R. Sudderth
źródło