Funktionsweise des nicht blockierenden E / A-Modells mit einem Thread in Node.js.


325

Ich bin kein Knotenprogrammierer, aber ich bin daran interessiert, wie das nicht blockierende E / A-Modell mit einem Thread funktioniert. Nachdem ich den Artikel " Verständnis der Knoten-js-Ereignisschleife" gelesen habe , bin ich wirklich verwirrt darüber. Es gab ein Beispiel für das Modell:

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: Wenn zwei Anforderungen A (kommt zuerst) und B vorhanden sind, da nur ein einziger Thread vorhanden ist, verarbeitet das serverseitige Programm die Anforderung A. Erstens: Beim Ausführen von SQL-Abfragen wird die Anweisung "Sleep" für E / A-Wartezeit ausgeführt. Und das Programm bleibt beim I/OWarten hängen und kann den Code, der die dahinter stehende Webseite rendert, nicht ausführen. Wird das Programm während des Wartens auf Anforderung B umschalten? Meiner Meinung nach gibt es aufgrund des Single-Thread-Modells keine Möglichkeit, eine Anforderung von einer anderen zu wechseln. Der Titel des Beispielcodes besagt jedoch, dass bis auf Ihren Code alles parallel läuft .

(PS Ich bin mir nicht sicher, ob ich den Code falsch verstehe oder nicht, da ich Node noch nie verwendet habe.) Wie wechselt Node während des Wartens von A nach B. Und können Sie das Single-Threaded-IO-Modell von Node auf einfache Weise erklären ? Ich würde mich freuen, wenn Sie mir helfen könnten. :) :)

Antworten:


374

Node.js basiert auf libuv , einer plattformübergreifenden Bibliothek, die apis / syscalls für asynchrone (nicht blockierende) Ein- / Ausgaben abstrahiert, die von den unterstützten Betriebssystemen (mindestens Unix, OS X und Windows) bereitgestellt werden.

Asynchrone E / A.

In diesem Programmiermodell blockieren Öffnungs- / Lese- / Schreibvorgänge auf Geräten und Ressourcen (Sockets, Dateisysteme usw.), die vom Dateisystem verwaltet werden , nicht den aufrufenden Thread (wie im typischen synchronen c-ähnlichen Modell) und markieren nur die Prozess (in der Datenstruktur auf Kernel- / Betriebssystemebene), der benachrichtigt wird, wenn neue Daten oder Ereignisse verfügbar sind. Im Falle einer Webserver-ähnlichen App ist der Prozess dann dafür verantwortlich, herauszufinden, zu welcher Anforderung / welchem ​​Kontext das gemeldete Ereignis gehört, und die Anforderung von dort aus zu verarbeiten. Beachten Sie, dass dies zwangsläufig bedeutet, dass Sie sich auf einem anderen Stapelrahmen befinden als dem, von dem die Anforderung an das Betriebssystem stammt, da dieser einem Prozess-Dispatcher nachgeben musste, damit ein einzelner Thread-Prozess neue Ereignisse verarbeiten kann.

Das Problem mit dem von mir beschriebenen Modell ist, dass es für den Programmierer nicht vertraut und schwer zu überlegen ist, da es nicht sequentieller Natur ist. "Sie müssen eine Anfrage in Funktion A stellen und das Ergebnis in einer anderen Funktion verarbeiten, in der Ihre Einheimischen von A normalerweise nicht verfügbar sind."

Knotenmodell (Continuation Passing Style und Event Loop)

Node behebt das Problem, indem die Sprachfunktionen von Javascript genutzt werden, um dieses Modell ein wenig synchroner zu gestalten, indem der Programmierer veranlasst wird, einen bestimmten Programmierstil zu verwenden. Jede Funktion, die E / A anfordert, hat eine Signatur wie function (... parameters ..., callback)und muss einen Rückruf erhalten, der aufgerufen wird, wenn der angeforderte Vorgang abgeschlossen ist (denken Sie daran, dass die meiste Zeit darauf gewartet wird, dass das Betriebssystem die Fertigstellung signalisiert - die Zeit, die sein kann verbrachte andere Arbeit). Durch die Unterstützung von Javascript für Schließungen können Sie Variablen verwenden, die Sie in der äußeren (aufrufenden) Funktion innerhalb des Rückrufkörpers definiert haben. Auf diese Weise können Sie den Status zwischen verschiedenen Funktionen beibehalten, die von der Knotenlaufzeit unabhängig voneinander aufgerufen werden. Siehe auch Continuation Passing Style .

Darüber hinaus returnsteuert die aufrufende Funktion nach dem Aufrufen einer Funktion, die eine E / A-Operation erzeugt, normalerweise die Ereignisschleife des Knotens . Diese Schleife ruft den nächsten Rückruf oder die nächste Funktion auf, deren Ausführung geplant war (höchstwahrscheinlich, weil das entsprechende Ereignis vom Betriebssystem benachrichtigt wurde). Dies ermöglicht die gleichzeitige Verarbeitung mehrerer Anforderungen.

Sie können sich die Ereignisschleife des Knotens als etwas ähnlich wie den Dispatcher des Kernels vorstellen: Der Kernel würde die Ausführung eines blockierten Threads planen, sobald seine ausstehende E / A abgeschlossen ist, während der Knoten einen Rückruf plant, wenn das entsprechende Ereignis aufgetreten ist.

Sehr gleichzeitig, keine Parallelität

Als letzte Bemerkung macht der Ausdruck "alles läuft parallel außer Ihrem Code" eine anständige Aufgabe, den Punkt zu erfassen, an dem der Knoten Ihrem Code ermöglicht, Anforderungen von Hunderttausenden offenen Sockets mit einem einzigen Thread gleichzeitig zu verarbeiten, indem alle Ihre js gemultiplext und sequenziert werden Logik in einem einzigen Ausführungsstrom (obwohl die Aussage "alles läuft parallel" hier wahrscheinlich nicht korrekt ist - siehe Parallelität vs. Parallelität - Was ist der Unterschied? ). Dies funktioniert ziemlich gut für Webapp-Server, da die meiste Zeit tatsächlich für das Warten auf Netzwerk oder Festplatte (Datenbank / Sockets) aufgewendet wird und die Logik nicht wirklich CPU-intensiv ist - das heißt: Dies funktioniert gut für E / A-gebundene Workloads .


45
Eine Folgefrage: Wie geschieht die E / A dann tatsächlich? Der Knoten stellt eine Anfrage an das System und bittet um Benachrichtigung, wenn es fertig ist. Führt das System also einen Thread aus, der die E / A ausführt, oder führt das System die E / A auch auf Hardwareebene mithilfe von Interrupts asynchron aus? Irgendwo muss etwas warten, bis die E / A beendet ist, und das wird blockiert, bis es fertig ist und eine gewisse Menge an Ressourcen verbraucht.
Philip

6
Ich habe gerade bemerkt, dass dieser nachfolgende Kommentar von @ user568109 unten beantwortet wird. Ich wünschte, es gäbe eine Möglichkeit, diese beiden Antworten zusammenzuführen.
lfalin

4
Ich wünschte, Sie könnten eine Antwort doppelt so lange schreiben, damit ich sie doppelt so gut verstehe.
Rafael Eyng

Der Knoten wird an vielen Stellen unterstützt. Als ich Firmware für MIPS32-Router entwarf, konnte Node.JS auf diesen über OpenWRT ausgeführt werden.
Qix - MONICA wurde

Wie punktet es über Apache? Apache kann auch gleichzeitige Verbindungen mit einem separaten Thread verarbeiten.
Suhail Gupta

209

Um eine Perspektive zu geben, lassen Sie mich node.js mit Apache vergleichen.

Apache ist ein HTTP-Server mit mehreren Threads. Für jede Anforderung, die der Server empfängt, wird ein separater Thread erstellt, der diese Anforderung verarbeitet.

Node.js hingegen ist ereignisgesteuert und verarbeitet alle Anforderungen asynchron von einem einzelnen Thread.

Wenn A und B auf Apache empfangen werden, werden zwei Threads erstellt, die Anforderungen verarbeiten. Jeder behandelt die Abfrage separat und wartet auf die Abfrageergebnisse, bevor er die Seite bereitstellt. Die Seite wird nur bereitgestellt, bis die Abfrage abgeschlossen ist. Der Abfrageabruf wird blockiert, da der Server den Rest des Threads erst ausführen kann, wenn er das Ergebnis empfängt.

Im Knoten wird c.query asynchron behandelt, dh während c.query die Ergebnisse für A abruft, springt es, um c.query für B zu verarbeiten, und wenn die Ergebnisse für A ankommen, sendet es die Ergebnisse an den Rückruf zurück, der das sendet Antwort. Node.js kann einen Rückruf ausführen, wenn der Abruf abgeschlossen ist.

Meiner Meinung nach gibt es keine Möglichkeit, von einer Anforderung zu einer anderen zu wechseln, da es sich um ein einzelnes Thread-Modell handelt.

Tatsächlich erledigt der Knotenserver immer genau das für Sie. Um Schalter zu erstellen (das asynchrone Verhalten), haben die meisten Funktionen, die Sie verwenden würden, Rückrufe.

Bearbeiten

Die SQL-Abfrage stammt aus der MySQL- Bibliothek. Es implementiert den Rückrufstil sowie den Ereignisemitter, um SQL-Anforderungen in die Warteschlange zu stellen. Sie werden nicht asynchron ausgeführt, sondern von den internen libuv- Threads, die die Abstraktion nicht blockierender E / A bereitstellen. Die folgenden Schritte werden ausgeführt, um eine Abfrage durchzuführen:

  1. Öffnen Sie eine Verbindung zu db, die Verbindung selbst kann asynchron hergestellt werden.
  2. Sobald die Datenbank verbunden ist, wird die Abfrage an den Server weitergeleitet. Abfragen können in die Warteschlange gestellt werden.
  3. Die Hauptereignisschleife wird über den Abschluss mit Rückruf oder Ereignis benachrichtigt.
  4. Die Hauptschleife führt Ihren Rückruf- / Ereignishandler aus.

Die eingehenden Anforderungen an den http-Server werden auf ähnliche Weise behandelt. Die interne Thread-Architektur sieht ungefähr so ​​aus:

node.js Ereignisschleife

Die C ++ - Threads sind die libuv-Threads, die die asynchrone E / A (Festplatte oder Netzwerk) ausführen. Die Hauptereignisschleife wird nach dem Versenden der Anforderung an den Thread-Pool weiter ausgeführt. Es kann weitere Anfragen annehmen, da es nicht wartet oder schläft. SQL-Abfragen / HTTP-Anforderungen / Dateisystem-Lesevorgänge erfolgen auf diese Weise.


16
Diagramm ist sehr nützlich.
Anmol Saraf

14
Warten Sie, also haben Sie in Ihrem Diagramm den "internen C ++ - Threadpool", was bedeutet, dass alle E / A-Blockierungsvorgänge einen Thread erzeugen, oder? Wenn meine Node-App für jede Anforderung E / A-Vorgänge ausführt , gibt es praktisch keinen Unterschied zwischen dem Node-Modell und dem Apache-Modell? Ich bekomme diesen Teil nicht leid.
Gav.Newalkar

21
@ gav.newalkar Sie erzeugen keinen Thread, die Anforderungen werden in die Warteschlange gestellt. Die Threads im Threadpool verarbeiten sie. Die Threads sind nicht dynamisch und pro Anforderung wie in Apache. Sie sind normalerweise fest und unterscheiden sich von System zu System.
user568109

10
@ user568109 Aber Apache verwendet auch einen Threadpool ( httpd.apache.org/docs/2.4/mod/worker.html ). Am Ende unterscheidet sich der Unterschied zwischen einem Setup mit node.js von einem mit Apache vorne nur dort, wo sich der Threadpool befindet, nicht wahr?
Kris

13
Dieses Diagramm sollte sich auf der ersten Seite der offiziellen Dokumente befinden.
Bouvierr

52

Node.js verwendet libuv hinter den Kulissen. libuv hat einen Thread-Pool (standardmäßig Größe 4). Daher verwendet Node.js Threads , um Parallelität zu erreichen.

Jedoch , Ihr Code läuft auf einem einzigen Thread (dh, alle der Rückrufe von Node.js Funktionen auf dem gleichen Thread aufgerufen werden, der sogenannte Loop-Thread oder Event-Loop). Wenn Leute sagen "Node.js läuft auf einem einzelnen Thread", sagen sie wirklich "die Rückrufe von Node.js laufen auf einem einzelnen Thread".


1
Kurze aber klare Antwort (y)
Sudhanshu Gaur

1
Gute Antwort Ich würde hinzufügen, dass E / A außerhalb dieser Hauptereignisschleife, Schleifenthread, Anforderungsthread stattfindet
Ionut Popa

Das ist die Antwort, nach der ich nach 2 Stunden gesucht habe, wie die Parallelität in einer Single-Threaded-Anwendung verwaltet wurde
Muhammad Ramzan

Ja, es ist schwer, die Antwort auf die nächste Stufe zu bekommen. Dies erklärt, wo die E / A tatsächlich ausgeführt wird (in einem Thread-Pool woanders)
Oliver Shaw

9

Node.js basiert auf dem Programmiermodell für Ereignisschleifen. Die Ereignisschleife wird in einem einzelnen Thread ausgeführt, wartet wiederholt auf Ereignisse und führt dann alle Ereignishandler aus, die diese Ereignisse abonniert haben. Ereignisse können zum Beispiel sein

  • Das Warten auf den Timer ist abgeschlossen
  • Der nächste Datenblock kann in diese Datei geschrieben werden
  • Es kommt eine neue HTTP-Anfrage auf uns zu

All dies läuft in einem einzigen Thread und es wird niemals parallel JavaScript-Code ausgeführt. Solange diese Event-Handler klein sind und auf weitere Events warten, funktioniert alles gut. Auf diese Weise können mehrere Anforderungen gleichzeitig von einem einzelnen Node.js-Prozess verarbeitet werden.

(Es gibt ein bisschen Magie unter der Haube, als wo die Ereignisse entstehen. Einige davon beinhalten parallel laufende Worker-Threads auf niedriger Ebene.)

In diesem SQL-Fall passieren viele Dinge (Ereignisse) zwischen der Datenbankabfrage und dem Abrufen der Ergebnisse im Rückruf . Während dieser Zeit pumpt die Ereignisschleife immer wieder Leben in die Anwendung und leitet andere Anforderungen jeweils um ein winziges Ereignis weiter. Daher werden mehrere Anforderungen gleichzeitig bearbeitet.

Ereignisschleife auf hoher Ebene

Laut: "Ereignisschleife von 10.000 Fuß - Kernkonzept hinter Node.js" .


5

Die Funktion c.query () hat zwei Argumente

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

Die Operation "Daten abrufen" ist in diesem Fall eine DB-Abfrage. Diese kann jetzt von Node.js behandelt werden, indem ein Arbeitsthread erzeugt und ihm die Aufgabe übertragen wird, die DB-Abfrage auszuführen. (Denken Sie daran, dass Node.js einen Thread intern erstellen kann.) Dadurch kann die Funktion sofort und ohne Verzögerung zurückkehren

Das zweite Argument "Nachbearbeitung von Daten" ist eine Rückruffunktion, das Node Framework registriert diesen Rückruf und wird von der Ereignisschleife aufgerufen.

Somit wird die Anweisung c.query (paramenter1, parameter2)sofort zurückgegeben, sodass der Knoten eine andere Anforderung bearbeiten kann.

PS: Ich habe gerade angefangen, Node zu verstehen, eigentlich wollte ich dies als Kommentar an @Philip schreiben, aber da ich nicht genug Reputationspunkte hatte, schrieb ich es als Antwort.


3

Wenn Sie etwas weiter lesen - "Natürlich gibt es im Backend Threads und Prozesse für den DB-Zugriff und die Prozessausführung. Diese sind jedoch nicht explizit für Ihren Code verfügbar, sodass Sie sich nur durch Wissen darum kümmern können dass E / A-Interaktionen, z. B. mit der Datenbank oder mit anderen Prozessen, aus Sicht jeder Anforderung asynchron sind, da die Ergebnisse dieser Threads über die Ereignisschleife an Ihren Code zurückgegeben werden. "

about - "Alles läuft parallel außer Ihrem Code" - Ihr Code wird synchron ausgeführt, wenn Sie eine asynchrone Operation aufrufen, z. B. auf E / A warten, behandelt die Ereignisschleife alles und ruft den Rückruf auf. Es ist einfach nichts, worüber man nachdenken muss.

In Ihrem Beispiel: Es gibt zwei Anforderungen A (kommt zuerst) und B. Sie führen Anforderung A aus, Ihr Code wird weiterhin synchron ausgeführt und Anforderung B ausgeführt. Die Ereignisschleife verarbeitet Anforderung A, wenn sie beendet ist, und ruft den Rückruf von Anforderung A mit auf das Ergebnis geht gleich an Anfrage B.


3
"Natürlich gibt es im Backend Threads und Prozesse für den DB-Zugriff und die Prozessausführung. Diese sind jedoch nicht explizit für Ihren Code verfügbar." - Wenn ich diesen Satz entnehme, sehe ich keinen Unterschied zwischen dem Knoten do oder ein Multithread-Framework - sagen wir Javas Spring Framework - tut es. Es gibt Threads, aber Sie steuern deren Erstellung nicht.
Rafael Eyng

@RafaelEyng Ich denke, für die Bearbeitung der Reihe mehrerer Anfragen wird der Knoten immer einen einzigen Thread dafür haben. Ich bin nicht sicher, ob jeder Rückruf neben anderen Prozessen wie dem Datenbankzugriff auf eine neue Instanz von Threads gestellt wird, aber zumindest wissen wir sicher, dass der Knoten Threads nicht jedes Mal instanziiert, wenn er eine Anforderung empfängt, die vor der Verarbeitung in der Schlange stehen muss (Ausführungen vorher) der Rückruf).
Kalter Cerberus

1

Okay, die meisten Dinge sollten bisher klar sein ... der schwierige Teil ist das SQL : Wenn es in Wirklichkeit nicht in einem anderen Thread oder Prozess in seiner Gesamtheit ausgeführt wird, muss die SQL-Ausführung in einzelne Schritte unterteilt werden (durch ein SQL - Prozessor für asynchrone Ausführung gemacht!), in dem die nicht-blockierenden diejenigen , ausgeführt werden, und die Sperr diejenigen (zB Schlaf) tatsächlich kann an den Kernel (als Alarm - Interrupt / Ereignis) und für die auf der Ereignisliste setzen übertragen werden Hauptschleife.

Das heißt, z. B. erfolgt die Interpretation von SQL usw. sofort, jedoch während des Wartens (gespeichert als Ereignis, das in Zukunft vom Kernel in einer Kqueue-, Epoll-, ... Struktur zusammen mit den anderen E / A-Operationen kommen wird ) Die Hauptschleife kann andere Dinge tun und schließlich prüfen, ob etwas von diesen E / A passiert ist und wartet.

Um es noch einmal umzuformulieren: Das Programm bleibt niemals hängen, Schlafaufrufe werden niemals ausgeführt. Ihre Aufgabe wird vom Kernel (etwas schreiben, warten, bis etwas über das Netzwerk kommt, warten, bis die Zeit abgelaufen ist) oder einem anderen Thread oder Prozess erledigt. - Der Knotenprozess prüft, ob mindestens eine dieser Aufgaben vom Kernel in dem einzigen blockierenden Aufruf an das Betriebssystem einmal in jedem Ereignisschleifenzyklus beendet wird. Dieser Punkt ist erreicht, wenn alles, was nicht blockiert ist, erledigt ist.

Klar? :-)

Ich kenne Node nicht. Aber woher kommt die c.query?


kqueue epoll dient zur skalierbaren asynchronen E / A-Benachrichtigung im Linux-Kernel. Node hat dafür libuv. Der Knoten befindet sich vollständig im Benutzerland. Es hängt nicht davon ab, was der Kernel implementiert.
user568109

1
@ user568109, libuv ist der mittlere Mann von Node. Jedes asynchrone Framework hängt (direkt oder nicht) von einer asynchronen E / A-Unterstützung im Kernel ab. Damit?
Robert Siemer

Entschuldigung für die Verwirrung. Socket-Vorgänge erfordern nicht blockierende E / A vom Kernel. Es kümmert sich um die asynchrone Handhabung. Asynchrone Datei-E / A werden jedoch von libuv selbst verarbeitet. Ihre Antwort sagt das nicht. Es behandelt beide als gleich und wird vom Kernel behandelt.
user568109
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.