Wann wird der Thread-Pool verwendet?

104

Ich verstehe also, wie Node.js funktioniert: Es hat einen einzelnen Listener-Thread, der ein Ereignis empfängt und es dann an einen Worker-Pool delegiert. Der Arbeitsthread benachrichtigt den Listener, sobald die Arbeit abgeschlossen ist, und der Listener gibt die Antwort an den Anrufer zurück.

Meine Frage lautet: Wenn ich einen HTTP-Server in Node.js aufrichte und bei einem meiner gerouteten Pfadereignisse (z. B. "/ test / sleep") den Ruhezustand aufruft, kommt das gesamte System zum Stillstand. Sogar der einzelne Listener-Thread. Mein Verständnis war jedoch, dass dieser Code im Worker-Pool vorkommt.

Im Gegensatz dazu sind DB-Lesevorgänge eine teure E / A-Operation, wenn ich Mongoose verwende, um mit MongoDB zu sprechen. Der Knoten scheint in der Lage zu sein, die Arbeit an einen Thread zu delegieren und den Rückruf zu erhalten, wenn er abgeschlossen ist. Die zum Laden aus der Datenbank benötigte Zeit scheint das System nicht zu blockieren.

Wie entscheidet sich Node.js für die Verwendung eines Thread-Pool-Threads im Vergleich zum Listener-Thread? Warum kann ich keinen Ereigniscode schreiben, der in den Ruhezustand wechselt und nur einen Thread-Pool-Thread blockiert?

Haney
quelle
@Tobi - das habe ich gesehen. Es beantwortet meine Frage immer noch nicht. Wenn die Arbeit an einem anderen Thread ausgeführt würde, würde der Ruhezustand nur diesen Thread und nicht auch den Listener betreffen.
Haney
8
Eine echte Frage, bei der Sie versuchen, etwas selbst zu verstehen, und wenn Sie keinen Ausgang zum Labyrinth finden, bitten Sie um Hilfe.
Rafael Eyng

Antworten:

240

Ihr Verständnis der Funktionsweise von Knoten ist nicht korrekt ... aber es ist ein weit verbreitetes Missverständnis, da die Realität der Situation tatsächlich ziemlich komplex ist und sich in der Regel auf markige kleine Sätze wie "Knoten ist Single-Threaded" beschränkt, die die Dinge zu stark vereinfachen .

Im Moment werden wir explizite Multiverarbeitung / Multithreading durch Cluster- und Webworker-Threads ignorieren und nur über typische Knoten ohne Thread sprechen.

Der Knoten wird in einer einzelnen Ereignisschleife ausgeführt. Es ist Single-Threaded, und Sie bekommen immer nur diesen einen Thread. Das gesamte von Ihnen geschriebene Javascript wird in dieser Schleife ausgeführt. Wenn in diesem Code eine Blockierungsoperation ausgeführt wird, wird die gesamte Schleife blockiert, und bis zum Abschluss wird nichts anderes ausgeführt. Dies ist die typische Single-Threaded-Natur des Knotens, von der Sie so viel hören. Aber es ist nicht das ganze Bild.

Bestimmte Funktionen und Module, die normalerweise in C / C ++ geschrieben sind, unterstützen asynchrone E / A. Wenn Sie diese Funktionen und Methoden aufrufen, verwalten sie intern die Weiterleitung des Aufrufs an einen Arbeitsthread. Wenn Sie beispielsweise das fsModul zum Anfordern einer Datei verwenden, fsleitet das Modul diesen Aufruf an einen Worker-Thread weiter, und dieser Worker wartet auf seine Antwort, die er dann an die Ereignisschleife zurückgibt, die ohne sie in der Datei weitergeleitet wurde inzwischen. All dies wird von Ihnen, dem Knotenentwickler, abstrahiert, und ein Teil davon wird durch die Verwendung von libuv von den Modulentwicklern abstrahiert .

Wie Denis Dollfus in den Kommentaren hervorhob (von dieser Antwort auf eine ähnliche Frage), ist die Strategie, die libuv verwendet, um asynchrone E / A zu erreichen, nicht immer ein Thread-Pool, insbesondere im Fall des httpModuls scheint eine andere Strategie zu sein zu diesem Zeitpunkt verwendet. Für unsere Zwecke hier ist es hauptsächlich wichtig zu beachten, wie der asynchrone Kontext erreicht wird (durch Verwendung von libuv) und dass der von libuv verwaltete Thread-Pool eine von mehreren Strategien ist, die von dieser Bibliothek angeboten werden, um Asynchronität zu erreichen.


In diesem ausgezeichneten Artikel wird eine viel tiefere Analyse darüber durchgeführt, wie der Knoten Asynchronität erreicht, und einige verwandte potenzielle Probleme und wie man damit umgeht . Das meiste davon erweitert das, was ich oben geschrieben habe, aber es weist zusätzlich darauf hin:

  • Jedes externe Modul, das Sie in Ihr Projekt aufnehmen und das native C ++ und libuv verwendet, verwendet wahrscheinlich den Thread-Pool (denken Sie: Datenbankzugriff).
  • libuv hat eine Standard-Thread-Pool-Größe von 4 und verwendet eine Warteschlange, um den Zugriff auf den Thread-Pool zu verwalten. Das Ergebnis ist, dass, wenn 5 lang laufende DB-Abfragen gleichzeitig ausgeführt werden, eine davon (und jede andere asynchrone) Eine Aktion, die sich auf den Thread-Pool stützt, wartet darauf, dass diese Abfragen abgeschlossen sind, bevor sie überhaupt gestartet werden
  • Sie können dies abmildern, indem Sie die Größe des Thread-Pools über die UV_THREADPOOL_SIZEUmgebungsvariable erhöhen , sofern Sie dies tun, bevor der Thread-Pool benötigt und erstellt wird:process.env.UV_THREADPOOL_SIZE = 10;

Wenn Sie herkömmliche Multi-Processing- oder Multithreading-Funktionen im Knoten wünschen, können Sie diese über das integrierte clusterModul oder verschiedene andere Module wie die oben genannten abrufen webworker-threadsoder sie fälschen, indem Sie eine Methode implementieren, mit der Sie Ihre Arbeit aufteilen und manuell setTimeoutoder verwenden können setImmediateoder process.nextTickum Ihre Arbeit anzuhalten und in einer späteren Schleife fortzusetzen, damit andere Prozesse abgeschlossen werden können (dies wird jedoch nicht empfohlen).

Bitte beachten Sie, dass Sie wahrscheinlich einen Fehler machen, wenn Sie Code mit langer Laufzeit / Blockierung in Javascript schreiben. Andere Sprachen arbeiten viel effizienter.

Jason
quelle
1
Heiliger Mist, das klärt es für mich völlig auf. Vielen Dank @Jason!
Haney
5
Kein Problem :) Ich befand mich vor nicht allzu langer Zeit dort, wo Sie sind, und es war schwierig, zu einer genau definierten Antwort zu gelangen, da Sie auf der einen Seite C / C ++ - Entwickler haben, für die die Antwort offensichtlich ist, und auf der anderen Seite typisch Webentwickler, die sich noch nie zu tief mit solchen Fragen befasst haben. Ich bin mir nicht einmal sicher, ob meine Antwort zu 100% technisch korrekt ist, wenn Sie das C-Level erreichen, aber es stimmt in den großen Zügen.
Jason
3
Die Verwendung des Thread-Pools für Netzwerkanforderungen wäre eine enorme Ressourcenverschwendung. Laut dieser Frage "macht es die asynchrone Netzwerk-E / A basierend auf den asynchronen E / A-Schnittstellen in verschiedenen Plattformen wie Epoll, Kqueue und IOCP ohne Thread-Pool" - was Sinn macht.
Denis Dollfus
1
... das heißt, wenn Sie den Haupt-Javascript-Thread direkt stark anheben oder nicht über genügend Ressourcen verfügen oder diese nicht angemessen verwalten, um dem Threadpool genügend Headroom zu geben, können Sie eine Verzögerung bei einer geringeren Parallelität einführen Schwellenwert - das Ergebnis ist, dass bei denselben Systemressourcen bei node.js normalerweise ein höherer Thruput auftritt als bei anderen Optionen (obwohl es andere ereignisbasierte Systeme in anderen Sprachen gibt, die dies in Frage stellen möchten - habe ich nicht gesehene aktuelle Benchmarks) - es ist klar, dass ein ereignisbasiertes Modell ein Thread-Modell übertrifft.
Jason
1
@Aabid Der Listener-Thread führt keine Datenbankabfrage aus, daher dauert es ungefähr 6 Sekunden, bis alle 10 dieser Abfragen abgeschlossen sind (bei der Standardgröße des Thread-Pools von 4). Wenn Sie Arbeiten in Javascript ausführen müssen, für deren Abschluss die Ergebnisse dieser Datenbankabfrage nicht erforderlich sind, z. B. wenn weitere Anforderungen eingehen, für die keine asynchrone Arbeit vom Thread-Pool ausgeführt werden muss, funktioniert dies im Wesentlichen weiter Ereignisschleife.
Jason
20

Ich verstehe also, wie Node.js funktioniert: Es hat einen einzelnen Listener-Thread, der ein Ereignis empfängt und es dann an einen Worker-Pool delegiert. Der Arbeitsthread benachrichtigt den Listener, sobald die Arbeit abgeschlossen ist, und der Listener gibt die Antwort an den Anrufer zurück.

Das ist nicht wirklich genau. Node.js hat nur einen einzigen "Worker" -Thread, der Javascript ausführt. Es gibt Threads innerhalb des Knotens, die die E / A-Verarbeitung handhaben, aber sie als "Worker" zu betrachten, ist ein Missverständnis. Es gibt wirklich nur E / A-Behandlung und einige andere Details der internen Implementierung des Knotens, aber als Programmierer können Sie sein Verhalten nur durch einige andere Parameter wie MAX_LISTENERS beeinflussen.

Meine Frage lautet: Wenn ich einen HTTP-Server in Node.js aufrichte und bei einem meiner gerouteten Pfadereignisse (z. B. "/ test / sleep") den Ruhezustand aufruft, kommt das gesamte System zum Stillstand. Sogar der einzelne Listener-Thread. Mein Verständnis war jedoch, dass dieser Code im Worker-Pool vorkommt.

In JavaScript gibt es keinen Schlafmechanismus. Wir könnten dies konkreter diskutieren, wenn Sie einen Code-Ausschnitt dessen veröffentlichen, was Ihrer Meinung nach "Schlaf" bedeutet. Es gibt keine solche Funktion, die aufgerufen werden kann, um beispielsweise etwas time.sleep(30)in Python zu simulieren . Es gibt setTimeoutaber das ist grundsätzlich NICHT Schlaf. setTimeoutund die Ereignisschleife setIntervalexplizit freigeben , nicht blockieren, damit andere Codebits auf dem Hauptausführungsthread ausgeführt werden können. Das einzige, was Sie tun können, ist, die CPU mit In-Memory-Berechnungen zu schleifen, wodurch der Hauptausführungsthread tatsächlich ausgehungert wird und Ihr Programm nicht mehr reagiert.

Wie entscheidet sich Node.js für die Verwendung eines Thread-Pool-Threads im Vergleich zum Listener-Thread? Warum kann ich keinen Ereigniscode schreiben, der in den Ruhezustand wechselt und nur einen Thread-Pool-Thread blockiert?

Netzwerk-E / A ist immer asynchron. Ende der Geschichte. Festplatten-E / A verfügt sowohl über synchrone als auch über asynchrone APIs, sodass keine "Entscheidung" getroffen wird. node.js verhält sich gemäß den API-Kernfunktionen, die Sie als Synchronisierung bezeichnen, im Vergleich zu normaler Asynchronität. Zum Beispiel: fs.readFilevs fs.readFileSync. Für Kindprozesse gibt es auch separate child_process.execund child_process.execSyncAPIs.

Als Faustregel gilt immer die Verwendung der asynchronen APIs. Die gültigen Gründe für die Verwendung der Synchronisierungs-APIs sind Initialisierungscode in einem Netzwerkdienst, bevor er auf Verbindungen wartet, oder einfache Skripte, die keine Netzwerkanforderungen für Build-Tools und dergleichen akzeptieren.

Peter Lyons
quelle
1
Woher kommen diese asynchronen APIs? Ich verstehe, was Sie sagen, aber wer auch immer diese APIs geschrieben hat, hat sich für IOCP / async entschieden. Wie haben sie sich dafür entschieden?
Haney
3
Seine Frage ist, wie er seinen eigenen zeitintensiven Code schreiben und nicht blockieren würde.
Jason
1
Ja. Der Knoten bietet grundlegende UDP-, TCP- und HTTP-Netzwerke. Es bietet NUR asynchrone "poolbasierte" APIs. Der gesamte node.js-Code der Welt verwendet ausnahmslos diese poolbasierten asynchronen APIs, da einfach alles verfügbar ist. Dateisystem- und untergeordnete Prozesse sind eine andere Geschichte, aber die Vernetzung ist durchweg asynchron.
Peter Lyons
4
Vorsicht, Peter, damit du nicht der sprichwörtliche Topf für seinen Kessel bist. Er möchte wissen, wie es die Autoren der Netzwerk-API gemacht haben, nicht wie Leute, die die Netzwerk-API verwenden. Schließlich habe ich verstanden, wie sich Knoten bezüglich nicht blockierender Ereignisse verhalten, weil ich meinen eigenen nicht blockierenden Code schreiben wollte, der nichts mit dem Netzwerk oder einer der anderen integrierten asynchronen APIs zu tun hat. Es ist ziemlich klar, dass David dasselbe tun möchte.
Jason
2
Knoten verwendet keine Thread-Pools für E / A, es verwendet native nicht blockierende E / A, die einzige Ausnahme ist fs, soweit ich weiß
vkurchatkin
2

Thread-Pool wie wann und wer verwendet:

Wenn wir Node auf einem Computer verwenden / installieren, wird zunächst ein Prozess gestartet, der als Knotenprozess auf dem Computer bezeichnet wird, und er wird so lange ausgeführt, bis Sie ihn beenden. Und dieser laufende Prozess ist unser sogenannter Single Thread.

Geben Sie hier die Bildbeschreibung ein

Der Mechanismus eines einzelnen Threads erleichtert das Blockieren einer Knotenanwendung. Dies ist jedoch eine der einzigartigen Funktionen, die Node.js in die Tabelle einbringt. Wenn Sie also Ihre Knotenanwendung erneut ausführen, wird sie nur in einem einzigen Thread ausgeführt. Egal, ob 1 oder 1 Million Benutzer gleichzeitig auf Ihre Anwendung zugreifen.

Lassen Sie uns also genau verstehen, was im einzelnen Thread von nodejs passiert, wenn Sie Ihre Knotenanwendung starten. Zuerst wird das Programm initialisiert, dann wird der gesamte Code der obersten Ebene ausgeführt, dh alle Codes, die sich nicht in einer Rückruffunktion befinden ( denken Sie daran, dass alle Codes in allen Rückruffunktionen in der Ereignisschleife ausgeführt werden ).

Danach wird der gesamte Modulcode ausgeführt und der gesamte Rückruf registriert. Schließlich wurde die Ereignisschleife für Ihre Anwendung gestartet.

Geben Sie hier die Bildbeschreibung ein

Wie bereits erwähnt, werden alle Rückruffunktionen und Codes in diesen Funktionen in der Ereignisschleife ausgeführt. In der Ereignisschleife werden die Lasten in verschiedenen Phasen verteilt. Wie auch immer, ich werde hier nicht über die Ereignisschleife diskutieren.

Zum besseren Verständnis des Thread-Pools bitte ich Sie, sich vorzustellen, dass in der Ereignisschleife Codes innerhalb einer Rückruffunktion ausgeführt werden, nachdem die Ausführung von Codes innerhalb einer anderen Rückruffunktion abgeschlossen wurde. Wenn nun einige Aufgaben tatsächlich zu schwer sind. Sie würden dann den einzelnen Thread unseres Knotens blockieren. Und hier kommt der Thread-Pool ins Spiel, der genau wie die Ereignisschleife von der libuv-Bibliothek für Node.js bereitgestellt wird.

Der Thread-Pool ist also kein Teil von nodejs selbst. Er wird von libuv bereitgestellt, um schwere Aufgaben an libuv zu verlagern. Libuv führt diese Codes in seinen eigenen Threads aus und nach der Ausführung gibt libuv die Ergebnisse an das Ereignis in der Ereignisschleife zurück.

Geben Sie hier die Bildbeschreibung ein

Der Thread-Pool gibt uns vier zusätzliche Threads, die vollständig vom einzelnen Haupt-Thread getrennt sind. Und wir können es tatsächlich bis zu 128 Threads konfigurieren.

Alle diese Threads bildeten zusammen einen Threadpool. Die Ereignisschleife kann dann schwere Aufgaben automatisch in den Thread-Pool verlagern.

Der lustige Teil ist, dass dies alles automatisch hinter den Kulissen geschieht. Es sind nicht wir Entwickler, die entscheiden, was in den Thread-Pool geht und was nicht.

Es gibt viele Aufgaben, die an den Thread-Pool gehen, wie z

-> All operations dealing with files
->Everyting is related to cryptography, like caching passwords.
->All compression stuff
->DNS lookups
Herr
quelle
0

Dieses Missverständnis ist lediglich der Unterschied zwischen präventivem Multitasking und kooperativem Multitasking ...

Der Schlaf schaltet den gesamten Karneval aus, weil es wirklich eine Linie zu allen Fahrten gibt und Sie das Tor geschlossen haben. Stellen Sie sich das als "JS-Interpreter und einige andere Dinge" vor und ignorieren Sie die Threads ... für Sie gibt es nur einen Thread, ...

... also blockiere es nicht.

Gregory R. Sudderth
quelle