348 Stimmen

Wie das single threaded non blocking IO-Modell in Node.js funktioniert

Ich bin kein Node-Programmierer, aber ich bin daran interessiert, wie das single-threaded nicht-blockierende IO-Modell funktioniert. Nachdem ich den Artikel gelesen habe verstehen-der-knoten-js-event-schleife Ich bin wirklich verwirrt. 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 es zwei Anfragen A (kommt zuerst) und B gibt, da es nur einen einzigen Thread gibt, wird das serverseitige Programm zuerst die Anfrage A bearbeiten: die SQL-Abfrage ist eine schlafende Anweisung, die für I/O-Warten steht. Und das Programm bleibt bei der I/O und kann den Code, der die Webseite rendert, nicht ausführen. Wird das Programm während der Wartezeit zu Anforderung B wechseln? Meiner Meinung nach gibt es aufgrund des Single-Thread-Modells keine Möglichkeit, eine Anforderung von einer anderen zu trennen. Aber der Titel des Beispielcodes sagt, dass alles läuft parallel, außer Ihrem Code .

(P.S. Ich bin mir nicht sicher, ob ich den Code falsch verstanden habe, da ich Wie kann Node während des Wartens von A nach B wechseln? Und können Sie erklären das single-threaded nicht-blockierende IO-Modell eines Knotens in einer einfache Art und Weise? Ich würde es zu schätzen wissen, wenn Sie mir helfen könnten :)

398voto

Utaal Punkte 8264

Node.js baut auf libuv eine plattformübergreifende Bibliothek, die Apis/Syscalls für asynchrone (nicht blockierende) Ein-/Ausgabe abstrahiert, die von den unterstützten Betriebssystemen (Unix, OS X und Windows) bereitgestellt werden.

Asynchrone IO

In diesem Programmiermodell werden Öffnungs-/Lese-/Schreibvorgänge auf Geräten und Ressourcen (Sockets, Dateisystem usw.), die vom Dateisystem verwaltet werden, durchgeführt. den aufrufenden Thread nicht blockieren (wie im typischen synchronen c-ähnlichen Modell) und markieren Sie einfach den Prozess (in der Datenstruktur auf Kernel-/OS-Ebene), um benachrichtigt zu werden, wenn neue Daten oder Ereignisse verfügbar sind. Im Falle einer Webserver-ähnlichen Anwendung ist der Prozess dann dafür verantwortlich, herauszufinden, zu welcher Anfrage/welchem Kontext das benachrichtigte Ereignis gehört und die Anfrage von dort aus weiter zu verarbeiten. Beachten Sie, dass dies zwangsläufig bedeutet, dass Sie sich auf einem anderen Stack-Frame befinden als derjenige, der die Anfrage an das Betriebssystem gestellt hat, da letzterer einem Prozess-Dispatcher weichen musste, damit ein Single-Thread-Prozess neue Ereignisse bearbeiten kann.

Das Problem mit dem von mir beschriebenen Modell ist, dass es dem Programmierer nicht vertraut und schwer zu erklären ist, da es nicht sequentiell ist. "Sie müssen eine Anfrage in Funktion A stellen und das Ergebnis in einer anderen Funktion verarbeiten, in der Ihre lokalen Daten aus A normalerweise nicht verfügbar sind.

Knotenmodell (Fortsetzungsübergabe-Stil und Ereignisschleife)

Node geht das Problem an, indem es die Sprachfunktionen von Javascript nutzt, um dieses Modell etwas synchroner zu gestalten, indem es den Programmierer dazu veranlasst, einen bestimmten Programmierstil zu verwenden. Jede Funktion, die IO anfordert, hat eine Signatur wie function (... parameters ..., callback) und muss einen Rückruf erhalten, der aufgerufen wird, wenn die angeforderte Operation abgeschlossen ist (bedenken Sie, dass die meiste Zeit damit verbracht wird, auf das Betriebssystem zu warten, um die Fertigstellung zu signalisieren - Zeit, die für andere Arbeiten genutzt werden kann). Javascript's Unterstützung für Closures erlaubt es Ihnen, Variablen, die Sie in der äußeren (aufrufenden) Funktion definiert haben, innerhalb des Körpers des Callbacks zu verwenden - dies erlaubt es, den Status zwischen verschiedenen Funktionen zu halten, die von der Node-Laufzeit unabhängig voneinander aufgerufen werden. Siehe auch Fortsetzung des Passspiels .

Außerdem wird die aufrufende Funktion nach dem Aufruf einer Funktion, die eine IO-Operation auslöst, normalerweise return Steuerung zum Knoten Ereignisschleife . Diese Schleife ruft den nächsten Callback oder die nächste Funktion auf, die für die Ausführung vorgesehen war (höchstwahrscheinlich, weil das entsprechende Ereignis vom Betriebssystem gemeldet wurde) - dies ermöglicht die gleichzeitige Verarbeitung mehrerer Anfragen.

Sie können sich die Ereignisschleife von node wie folgt vorstellen ähnlich wie der Dispatcher des Kernels Der Kernel würde einen blockierten Thread zur Ausführung einplanen, sobald sein anstehendes IO abgeschlossen ist, während der Knoten einen Rückruf einplant, wenn das entsprechende Ereignis eingetreten ist.

Hochgradig konkurrierend, keine Parallelität

Abschließend lässt sich sagen, dass der Satz "alles läuft parallel, außer Ihrem Code" den Punkt gut wiedergibt, dass Node Ihrem Code erlaubt, Anfragen von Hunderttausende von offenen Sockets mit einem einzigen Thread durch Multiplexing und Sequenzierung all Ihrer js-Logik in einem einzigen Ausführungsstrom (auch wenn die Aussage "alles läuft parallel" hier wahrscheinlich nicht korrekt ist - siehe Gleichzeitigkeit vs. Parallelität - Was ist der Unterschied? ). Dies funktioniert ziemlich gut für Webapp-Server, da die meiste Zeit tatsächlich auf das Warten auf Netzwerk oder Festplatte (Datenbank / Sockets) verbracht wird und die Logik ist nicht wirklich CPU-intensiv - das heißt,: dies funktioniert gut bei IO-gebundenen Arbeitslasten .

220voto

user568109 Punkte 46053

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

Der Apache ist ein HTTP-Server mit mehreren Threads. Für jede Anfrage, die der Server erhält, wird ein eigener Thread erstellt, der diese Anfrage bearbeitet.

Node.js hingegen ist ereignisgesteuert und verarbeitet alle Anfragen asynchron von einem einzigen Thread aus.

Wenn A und B auf dem Apache empfangen werden, werden zwei Threads erstellt, die die Anfragen bearbeiten. Jeder bearbeitet die Abfrage separat und wartet auf die Abfrageergebnisse, bevor er die Seite ausliefert. Die Seite wird erst zugestellt, wenn die Abfrage beendet ist. Die Abfrage ist blockiert, da der Server den Rest des Threads nicht ausführen kann, bis er das Ergebnis erhält.

In Node wird c.query asynchron gehandhabt, d.h. während c.query die Ergebnisse für A abruft, springt es zur Behandlung von c.query für B, und wenn die Ergebnisse für A ankommen, sendet es die Ergebnisse an Callback zurück, das die Antwort sendet. Node.js weiß, dass Callback ausgeführt werden muss, wenn Fetch beendet ist.

Da es sich um ein Single-Thread-Modell handelt, gibt es meiner Meinung nach keine Möglichkeit, die von einer Anfrage zu einer anderen zu wechseln.

Genau das macht der Node-Server die ganze Zeit für Sie. Um Schalter zu machen, (das asynchrone Verhalten) die meisten Funktionen, die Sie verwenden würden, haben Callbacks.

bearbeiten

Die SQL-Abfrage stammt aus mysql Bibliothek. Sie implementiert einen Callback-Stil sowie einen Event Emitter, um SQL-Anfragen in eine Warteschlange zu stellen. Sie führt sie nicht asynchron aus, das wird von der internen libuv Threads, die die Abstraktion der nicht blockierenden E/A ermöglichen. Die folgenden Schritte laufen bei einer Abfrage ab:

  1. Öffnen Sie eine Verbindung zu einer Datenbank, die Verbindung selbst kann asynchron hergestellt werden.
  2. Sobald die Datenbank verbunden ist, wird die Anfrage an den Server weitergeleitet. Abfragen können in eine Warteschlange gestellt werden.
  3. Die Hauptereignisschleife wird durch einen Rückruf oder ein Ereignis über den Abschluss informiert.
  4. Die Hauptschleife führt Ihren Callback/Eventhandler aus.

Die eingehenden Anfragen an den http-Server werden auf ähnliche Weise behandelt. Die interne Thread-Architektur sieht in etwa so aus:

node.js event loop

Die C++-Threads sind die libuv-Threads, die die asynchrone E/A (Festplatte oder Netzwerk) ausführen. Die Hauptereignisschleife wird nach der Übergabe der Anforderung an den Thread-Pool weiter ausgeführt. Sie kann weitere Anfragen annehmen, da sie weder wartet noch schläft. SQL-Abfragen/HTTP-Anfragen/Dateisystem-Lesevorgänge laufen alle auf diese Weise ab.

63voto

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

Allerdings , Ihr Code auf einem einzigen Thread abläuft (d.h. alle Rückrufe von Node.js-Funktionen werden auf demselben Thread aufgerufen, dem so genannten Loop-Thread oder Event-Loop). Wenn Leute sagen "Node.js läuft auf einem einzigen Thread", meinen sie eigentlich "die Callbacks von Node.js laufen auf einem einzigen Thread".

9voto

pspi Punkte 10201

Node.js basiert auf dem Programmiermodell der Ereignisschleife. Die Ereignisschleife läuft in einem einzigen Thread und wartet wiederholt auf Ereignisse und führt dann alle Ereignis-Handler aus, die diese Ereignisse abonniert haben. Ereignisse können zum Beispiel sein

  • Timer-Wartezeit ist beendet
  • das nächste Datenpaket ist bereit, in diese Datei geschrieben zu werden
  • es kommt eine neue HTTP-Anfrage auf uns zu

All dies läuft in einem einzigen Thread und kein JavaScript-Code wird jemals parallel ausgeführt. Solange diese Event-Handler klein sind und auf weitere Ereignisse warten, funktioniert alles reibungslos. Dies ermöglicht die gleichzeitige Bearbeitung mehrerer Anfragen durch einen einzigen Node.js-Prozess.

(Es gibt ein bisschen Magie unter der Haube, woher die Ereignisse kommen. Einiges davon betrifft parallel laufende Worker-Threads auf niedriger Ebene).

In diesem SQL-Fall, zwischen der Datenbankabfrage und dem Abrufen der Ergebnisse im Callback passieren eine Menge Dinge (Ereignisse) . Während dieser Zeit pumpt die Ereignisschleife immer wieder Leben in die Anwendung und schiebt andere Anforderungen ein winziges Ereignis nach dem anderen vor. Daher werden mehrere Anfragen gleichzeitig bedient.

event loop high level view

Laut: "Ereignisschleife aus 10.000 Fuß Höhe - Kernkonzept hinter Node.js" .

6voto

dhiraj suvarna Punkte 496

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, die von Node.js gehandhabt werden kann, indem man einen Worker-Thread startet und ihm die Aufgabe gibt, die DB-Abfrage durchzuführen. (Zur Erinnerung: Node.js kann intern Threads erstellen). Dadurch kann die Funktion ohne Verzögerung sofort zurückkehren

Das zweite Argument "Post-Processing of Data" ist eine Callback-Funktion, die vom Node Framework registriert und von der Ereignisschleife aufgerufen wird.

Daher die Aussage c.query (paramenter1, parameter2) kehrt sofort zurück und ermöglicht es dem Knoten, eine weitere Anfrage zu bearbeiten.

P.S.: Ich habe gerade angefangen, den Knoten zu verstehen, eigentlich wollte ich dies als Kommentar zu @Philip aber da ich nicht genug Rufpunkte hatte, schrieb ich es als Antwort.

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X