Nodejs Ereignisschleife

141

Gibt es intern zwei Ereignisschleifen in der Architektur von nodejs?

  • libev / libuv
  • v8 Javascript-Ereignisschleife

Stellt der Knoten bei einer E / A-Anforderung die Anforderung an libeio in die Warteschlange, die wiederum die Verfügbarkeit von Daten über Ereignisse mit libev benachrichtigt und diese Ereignisse schließlich von der v8-Ereignisschleife mithilfe von Rückrufen behandelt werden?

Wie sind libev und libeio in die Architektur von nodejs integriert?

Gibt es eine Dokumentation, die ein klares Bild der internen Architektur von nodej vermittelt?

Tamil
quelle

Antworten:

175

Ich habe persönlich den Quellcode von node.js & v8 gelesen.

Ich bin auf ein ähnliches Problem wie Sie gestoßen, als ich versucht habe, die Architektur von node.js zu verstehen, um native Module zu schreiben.

Was ich hier poste, ist mein Verständnis von node.js und dies könnte auch ein bisschen aus der Bahn geraten.

  1. Libev ist die Ereignisschleife, die intern in node.js ausgeführt wird, um einfache Ereignisschleifenoperationen auszuführen. Es wurde ursprünglich für * nix-Systeme geschrieben. Libev bietet eine einfache, aber optimierte Ereignisschleife, auf der der Prozess ausgeführt werden kann. Mehr über libev können Sie hier lesen .

  2. LibEio ist eine Bibliothek zur asynchronen Ausführung von Eingaben. Es behandelt Filedeskriptoren, Datenhandler, Steckdosen etc. Sie mehr darüber hier lesen hier .

  3. LibUv ist eine Abstraktionsschicht über libeio, libev, c-ares (für DNS) und iocp (für Windows asynchronous-io). LibUv führt alle io und Ereignisse im Ereignispool aus, verwaltet und verwaltet sie. (im Falle von Libeio Threadpool). Sie sollten sich das Tutorial von Ryan Dahl auf libUv ansehen. Das wird für Sie sinnvoller, wenn es darum geht, wie libUv selbst funktioniert, und dann werden Sie verstehen, wie node.js auf libuv und v8 funktioniert.

Um nur die Javascript-Ereignisschleife zu verstehen, sollten Sie diese Videos ansehen

Um zu sehen, wie libeio mit node.js verwendet wird, um asynchrone Module zu erstellen, sollten Sie dieses Beispiel sehen .

Grundsätzlich geschieht in node.js, dass die v8-Schleife alle Javascript-Teile sowie C ++ - Module ausführt und verarbeitet [wenn sie in einem Hauptthread ausgeführt werden (gemäß der offiziellen Dokumentation ist node.js selbst Single-Threaded)]. Außerhalb des Hauptthreads behandeln libev und libeio dies im Threadpool, und libev stellt die Interaktion mit der Hauptschleife bereit. Nach meinem Verständnis hat node.js also eine permanente Ereignisschleife: das ist die v8-Ereignisschleife. Für asynchrone C ++ - Aufgaben wird ein Threadpool verwendet [via libeio & libev].

Beispielsweise:

eio_custom(Task,FLAG,AfterTask,Eio_REQUEST);

Was in allen Modulen erscheint, ruft normalerweise die Funktion Taskim Threadpool auf. Wenn es abgeschlossen ist, ruft es die AfterTaskFunktion im Hauptthread auf. Wohingegen Eio_REQUESTder Request - Handler , der eine Struktur / Objekt , dessen Motiv sein kann , ist die Kommunikation zwischen dem Haupt - Thread und Threadpool vorzusehen.

ShrekOverflow
quelle
Sich auf die Tatsache zu verlassen, dass libuv libev intern verwendet, ist ein guter Weg, um Ihren Code nicht plattformübergreifend zu machen. Sie sollten sich nur um die öffentliche Schnittstelle von libuv kümmern.
Raynos
1
@Raynos libuv möchte sicherstellen, dass mehrere Bibliotheken x-platfousing sind. Richtig ? Daher ist die Verwendung von libuv eine gute Idee
ShrekOverflow
1
@Abhishek From Doc process.nextTick- Rufen Sie in der nächsten Schleife um die Ereignisschleife diesen Rückruf auf. Dies ist kein einfacher Alias ​​für setTimeout (fn, 0), sondern viel effizienter. Auf welche Ereignisschleife bezieht sich das? V8-Ereignisschleife?
Tamil
2
Beachten Sie, dass libuv nicht mehr über libev implementiert ist .
Strcat
4
Gibt es eine Möglichkeit, diese Ereigniswarteschlange zu "sehen"? Ich möchte in der Lage sein, die Reihenfolge der Aufrufe auf dem Stapel zu sehen und zu sehen, wie neue Funktionen dort übertragen werden, um besser zu verstehen, was passiert. Gibt es eine Variable, die Ihnen sagt, was in die Ereigniswarteschlange verschoben wurde?
Tbarbe
20

Es sieht so aus, als hätten einige der besprochenen Entitäten (z. B. libev usw.) aufgrund der Tatsache, dass es eine Weile her ist, an Relevanz verloren, aber ich denke, die Frage hat immer noch großes Potenzial.

Lassen Sie mich versuchen, die Funktionsweise eines ereignisgesteuerten Modells anhand eines abstrakten Beispiels in einer abstrakten UNIX-Umgebung im heutigen Node-Kontext zu erklären.

Programmperspektive:

  • Die Skript-Engine startet die Ausführung des Skripts.
  • Jedes Mal, wenn eine CPU-gebundene Operation auftritt, wird sie in ihrer Vollständigkeit inline (reale Maschine) ausgeführt.
  • Jedes Mal, wenn eine E / A-gebundene Operation auftritt, werden die Anforderung und ihr Abschlusshandler bei einer 'Ereignismaschine' (virtuelle Maschine) registriert.
  • Wiederholen Sie die oben beschriebenen Vorgänge, bis das Skript endet. CPU-gebundener Betrieb - Inline-E / A-gebundene Operationen ausführen und wie oben an die Maschine anfordern.
  • Wenn die E / A abgeschlossen ist, werden die Listener zurückgerufen.

Die oben beschriebene Ereignismaschinerie wird als libuv AKA-Ereignisschleifen-Framework bezeichnet. Node nutzt diese Bibliothek, um sein ereignisgesteuertes Programmiermodell zu implementieren.

Knotenperspektive:

  • Haben Sie einen Thread, um die Laufzeit zu hosten.
  • Holen Sie sich das Benutzerskript.
  • Kompilieren Sie es in native [Leverage v8]
  • Laden Sie die Binärdatei und springen Sie in den Einstiegspunkt.
  • Der kompilierte Code führt die CPU-gebundenen Aktivitäten mithilfe von Programmierprimitiven inline aus.
  • Viele E / A- und Timer-bezogene Codes verfügen über native Wraps. Zum Beispiel Netzwerk-E / A.
  • Daher werden E / A-Aufrufe vom Skript an C ++ - Bridges weitergeleitet, wobei das E / A-Handle und der Completion-Handler als Argumente übergeben werden.
  • Der native Code übt die libuv-Schleife aus. Es erfasst die Schleife, stellt ein Ereignis auf niedriger Ebene in die Warteschlange, das die E / A darstellt, und einen nativen Callback-Wrapper in die libuv-Schleifenstruktur.
  • Der native Code kehrt zum Skript zurück - im Moment findet keine E / A statt!
  • Die obigen Punkte werden viele Male wiederholt, bis der gesamte Nicht-E / A-Code ausgeführt und der gesamte E / A-Code registriert ist.
  • Wenn im System nichts mehr auszuführen ist, übergibt der Knoten das Steuerelement an libuv
  • libuv wird aktiv, nimmt alle registrierten Ereignisse auf und fragt das Betriebssystem nach ihrer Funktionsfähigkeit ab.
  • Diejenigen, die in einem nicht blockierenden Modus für E / A bereit sind, werden abgeholt, E / A ausgeführt und ihre Rückrufe ausgegeben. Einer nach dem anderen.
  • Diejenigen, die noch nicht bereit sind (zum Beispiel ein Socket-Lesevorgang, für den der andere Endpunkt noch nichts geschrieben hat), werden weiterhin mit dem Betriebssystem geprüft, bis sie verfügbar sind.
  • Die Schleife behält intern einen ständig wachsenden Timer bei. Wenn die Anwendung einen verzögerten Rückruf anfordert (z. B. setTimeout), wird dieser interne Zeitgeberwert genutzt, um den richtigen Zeitpunkt für das Auslösen des Rückrufs zu berechnen.

Während die meisten Funktionen auf diese Weise bereitgestellt werden, werden einige (asynchrone Versionen) der Dateivorgänge mithilfe zusätzlicher Threads ausgeführt, die gut in die Bibliothek integriert sind. Während Netzwerk-E / A-Vorgänge in Erwartung eines externen Ereignisses warten können, z. B. wenn der andere Endpunkt mit Daten usw. antwortet, müssen die Dateivorgänge vom Knoten selbst ausgeführt werden. Wenn Sie beispielsweise eine Datei öffnen und warten, bis der fd mit Daten fertig ist, geschieht dies nicht, da tatsächlich niemand liest! Wenn Sie im Hauptthread aus der Datei inline lesen, kann dies möglicherweise andere Aktivitäten im Programm blockieren und sichtbare Probleme verursachen, da Dateivorgänge im Vergleich zu CPU-gebundenen Aktivitäten sehr langsam sind. Daher werden interne Worker-Threads (konfigurierbar über die Umgebungsvariable UV_THREADPOOL_SIZE) verwendet, um Dateien zu bearbeiten.

Hoffe das hilft.

Gireesh Punathil
quelle
Woher wusstest du diese Dinge? Kannst du mich auf die Quelle hinweisen?
Suraj Jain
18

Eine Einführung in libuv

Das Projekt node.js begann 2009 als eine vom Browser entkoppelte JavaScript-Umgebung. Mit Googles V8 und Marc Lehmanns libev kombinierte node.js ein E / A-Modell mit einer Sprache, die für den Programmierstil gut geeignet war. aufgrund der Art und Weise, wie es von Browsern geformt worden war. Da node.js immer beliebter wurde, war es wichtig, dass es unter Windows funktioniert, aber libev lief nur unter Unix. Das Windows-Äquivalent zu Kernel-Ereignisbenachrichtigungsmechanismen wie kqueue oder (e) poll ist IOCP. libuv war je nach Plattform eine Abstraktion um libev oder IOCP, die Benutzern eine auf libev basierende API bereitstellte. In der node-v0.9.0-Version von libuv wurde libev entfernt .

Auch ein Bild, das die Ereignisschleife in Node.js von @ BusyRich beschreibt


Update 05/09/2017

Gemäß diesem Dokument Node.js Ereignisschleife ,

Das folgende Diagramm zeigt eine vereinfachte Übersicht über die Reihenfolge der Operationen der Ereignisschleife.

   ┌───────────────────────┐
┌─>│        timers         
  └──────────┬────────────┘
  ┌──────────┴────────────┐
       I/O callbacks     
  └──────────┬────────────┘
  ┌──────────┴────────────┐
       idle, prepare     
  └──────────┬────────────┘      ┌───────────────┐
  ┌──────────┴────────────┐         incoming:   
           poll          │<─────┤  connections, 
  └──────────┬────────────┘         data, etc.  
  ┌──────────┴────────────┐      └───────────────┘
          check          
  └──────────┬────────────┘
  ┌──────────┴────────────┐
└──┤    close callbacks    
   └───────────────────────┘

Hinweis: Jede Box wird als "Phase" der Ereignisschleife bezeichnet.

Phasenübersicht

  • Timer : In dieser Phase werden von setTimeout()und geplante Rückrufe ausgeführt setInterval().
  • E / A-Rückrufe : Führt fast alle Rückrufe aus, mit Ausnahme von Rückrufen , die von Zeitgebern geplant wurden, und setImmediate().
  • Leerlauf, vorbereiten : nur intern verwendet.
  • Umfrage : Neue E / A-Ereignisse abrufen; Der Knoten wird hier gegebenenfalls blockiert.
  • check : setImmediate()Rückrufe werden hier aufgerufen.
  • Rückrufe schließen : z socket.on('close', ...).

Zwischen jedem Durchlauf der Ereignisschleife prüft Node.js, ob es auf asynchrone E / A oder Timer wartet, und fährt sauber herunter, wenn keine vorhanden sind.

zangw
quelle
Sie haben das " In the node-v0.9.0 version of libuv libev was removed" zitiert , aber es gibt keine Beschreibung in nodejs changelog. github.com/nodejs/node/blob/master/CHANGELOG.md . Und wenn libev entfernt wird, wie wird dann asynchrone E / A in nodejs ausgeführt?
Integekhab
@intekhab, Über diesen Link denke ich, dass die auf libeio basierende libuv als Ereignisschleife in node.js verwendet werden könnte.
Zangw
@intekhab Ich denke, libuv implementiert alle Funktionen in Bezug auf E / A und Polling. Hier checken Sie in diesem Dokument ein: docs.libuv.org/en/v1.x/loop.html
Mohit Kaushik
13

In der NodeJs-Architektur gibt es eine Ereignisschleife.

Node.js Ereignisschleifenmodell

Knotenanwendungen werden in einem ereignisgesteuerten Single-Thread-Modell ausgeführt. Node implementiert jedoch einen Thread-Pool im Hintergrund, damit Arbeiten ausgeführt werden können.

Node.js fügt einer Ereigniswarteschlange Arbeit hinzu und lässt sie dann von einem einzelnen Thread ausführen, der eine Ereignisschleife ausführt. Die Ereignisschleife erfasst das oberste Element in der Ereigniswarteschlange, führt es aus und erfasst dann das nächste Element.

Wenn Sie Code ausführen, der länger lebt oder blockierende E / A aufweist, anstatt die Funktion direkt aufzurufen, wird die Funktion zusammen mit einem Rückruf zur Ereigniswarteschlange hinzugefügt, der nach Abschluss der Funktion ausgeführt wird. Wenn alle Ereignisse in der Ereigniswarteschlange von Node.js ausgeführt wurden, wird die Anwendung von Node.js beendet.

In der Ereignisschleife treten Probleme auf, wenn unsere Anwendungsfunktionen die E / A blockieren.

Node.js verwendet Ereignisrückrufe, um zu vermeiden, dass auf das Blockieren von E / A gewartet werden muss. Daher werden alle Anforderungen, die blockierende E / A ausführen, in einem anderen Thread im Hintergrund ausgeführt.

Wenn ein Ereignis, das E / A blockiert, aus der Ereigniswarteschlange abgerufen wird, ruft Node.js einen Thread aus dem Thread-Pool ab und führt die Funktion dort anstelle des Hauptereignisschleifenthreads aus. Dies verhindert, dass die blockierende E / A die restlichen Ereignisse in der Ereigniswarteschlange aufhält.

Peter Hauge
quelle
8

Es gibt nur eine Ereignisschleife, die von libuv bereitgestellt wird. V8 ist nur eine JS-Laufzeit-Engine.

Warren Zhou
quelle
1

Als Javascript-Anfänger hatte ich auch den gleichen Zweifel, enthält NodeJS 2 Ereignisschleifen?. Nach langer Recherche und Diskussion mit einem der V8-Mitarbeiter erhielt ich die folgenden Konzepte.

  • Die Ereignisschleife ist ein grundlegendes abstraktes Konzept des JavaScript-Programmiermodells. Die V8-Engine bietet daher eine Standardimplementierung für die Ereignisschleife, die Einbettungen (Browser, Knoten) ersetzen oder erweitern können . Die V8-Standardimplementierung der Ereignisschleife finden Sie hier
  • In NodeJS gibt es nur eine Ereignisschleife , die von der Knotenlaufzeit bereitgestellt wird. Die Standard-Ereignisschleifenimplementierung von V8 wurde durch die NodeJS-Ereignisschleifenimplementierung ersetzt
arunjos007
quelle
0

Die pbkdf2Funktion verfügt über die JavaScript-Implementierung, delegiert jedoch die gesamte zu erledigende Arbeit an die C ++ - Seite.

env->SetMethod(target, "pbkdf2", PBKDF2);
  env->SetMethod(target, "generateKeyPairRSA", GenerateKeyPairRSA);
  env->SetMethod(target, "generateKeyPairDSA", GenerateKeyPairDSA);
  env->SetMethod(target, "generateKeyPairEC", GenerateKeyPairEC);
  NODE_DEFINE_CONSTANT(target, OPENSSL_EC_NAMED_CURVE);
  NODE_DEFINE_CONSTANT(target, OPENSSL_EC_EXPLICIT_CURVE);
  NODE_DEFINE_CONSTANT(target, kKeyEncodingPKCS1);
  NODE_DEFINE_CONSTANT(target, kKeyEncodingPKCS8);
  NODE_DEFINE_CONSTANT(target, kKeyEncodingSPKI);
  NODE_DEFINE_CONSTANT(target, kKeyEncodingSEC1);
  NODE_DEFINE_CONSTANT(target, kKeyFormatDER);
  NODE_DEFINE_CONSTANT(target, kKeyFormatPEM);
  NODE_DEFINE_CONSTANT(target, kKeyTypeSecret);
  NODE_DEFINE_CONSTANT(target, kKeyTypePublic);
  NODE_DEFINE_CONSTANT(target, kKeyTypePrivate);
  env->SetMethod(target, "randomBytes", RandomBytes);
  env->SetMethodNoSideEffect(target, "timingSafeEqual", TimingSafeEqual);
  env->SetMethodNoSideEffect(target, "getSSLCiphers", GetSSLCiphers);
  env->SetMethodNoSideEffect(target, "getCiphers", GetCiphers);
  env->SetMethodNoSideEffect(target, "getHashes", GetHashes);
  env->SetMethodNoSideEffect(target, "getCurves", GetCurves);
  env->SetMethod(target, "publicEncrypt",
                 PublicKeyCipher::Cipher<PublicKeyCipher::kPublic,
                                         EVP_PKEY_encrypt_init,
                                         EVP_PKEY_encrypt>);
  env->SetMethod(target, "privateDecrypt",
                 PublicKeyCipher::Cipher<PublicKeyCipher::kPrivate,
                                         EVP_PKEY_decrypt_init,
                                         EVP_PKEY_decrypt>);
  env->SetMethod(target, "privateEncrypt",
                 PublicKeyCipher::Cipher<PublicKeyCipher::kPrivate,
                                         EVP_PKEY_sign_init,
                                         EVP_PKEY_sign>);
  env->SetMethod(target, "publicDecrypt",
                 PublicKeyCipher::Cipher<PublicKeyCipher::kPublic,
                                         EVP_PKEY_verify_recover_init,
                                         EVP_PKEY_verify_recover>);

Ressource: https://github.com/nodejs/node/blob/master/src/node_crypto.cc

Das Libuv-Modul hat eine weitere Verantwortung, die für einige ganz bestimmte Funktionen in der Standardbibliothek relevant ist.

Bei einigen Standard-Bibliotheksfunktionsaufrufen entscheiden sich die Node C ++ - Seite und Libuv dafür, teure Berechnungen vollständig außerhalb der Ereignisschleife durchzuführen.

Stattdessen verwenden sie einen sogenannten Thread-Pool. Der Thread-Pool besteht aus einer Reihe von vier Threads, mit denen rechenintensive Aufgaben wie die pbkdf2Funktion ausgeführt werden können.

Standardmäßig erstellt Libuv 4 Threads in diesem Thread-Pool.

Zusätzlich zu den in der Ereignisschleife verwendeten Threads gibt es vier weitere Threads, mit denen teure Berechnungen ausgelagert werden können, die in unserer Anwendung ausgeführt werden müssen.

Viele der in der Node-Standardbibliothek enthaltenen Funktionen verwenden diesen Thread-Pool automatisch. Die pbkdf2Funktion ist eine davon.

Das Vorhandensein dieses Thread-Pools ist sehr wichtig.

Node ist also kein Single-Thread, da es andere Threads gibt, die Node für einige rechenintensive Aufgaben verwendet.

Wenn der Ereignispool für die rechenintensive Aufgabe verantwortlich war, konnte unsere Knotenanwendung nichts anderes tun.

Unsere CPU führt alle Anweisungen einzeln in einem Thread aus.

Mithilfe des Thread-Pools können wir andere Dinge innerhalb einer Ereignisschleife ausführen, während Berechnungen durchgeführt werden.

Daniel
quelle