Inside the Node.js Event Loop: Ein tiefer Tauchgang
Wenhao Wang
Dev Intern · Leapcell

Node.js Single-Thread Modell Erkundung
Node.js verwendet den ereignisgesteuerten und asynchronen I/O-Ansatz und erreicht so eine Single-Thread-, hochgradig gleichzeitige JavaScript-Laufzeitumgebung. Da ein einzelner Thread bedeutet, dass immer nur eine Sache gleichzeitig erledigt werden kann, wie erreicht Node.js mit nur einem Thread eine hohe Gleichzeitigkeit und asynchrone I/O? Dieser Artikel wird das Single-Thread-Modell von Node.js anhand dieser Frage untersuchen.
Strategien für hohe Gleichzeitigkeit
Im Allgemeinen besteht die Lösung für hohe Gleichzeitigkeit darin, ein Multi-Thread-Modell bereitzustellen. Der Server weist jedem Client-Request einen Thread zu und verwendet synchrone I/O. Das System kompensiert die Zeitkosten für synchrone I/O-Aufrufe durch Thread-Switching. Apache verwendet beispielsweise diese Strategie. Da I/O-Operationen normalerweise zeitaufwändig sind, ist es schwierig, mit diesem Ansatz eine hohe Leistung zu erzielen. Es ist jedoch sehr einfach und kann komplexe Interaktionslogiken implementieren.
Tatsächlich führen die meisten Webserver-Seiten nicht viele Berechnungen durch. Nach dem Empfangen von Requests leiten sie die Requests an andere Dienste weiter (z. B. Lesen von Datenbanken), warten dann auf die Rückgabe der Ergebnisse und senden die Ergebnisse schließlich an die Clients. Daher verwendet Node.js ein Single-Thread-Modell, um diese Situation zu bewältigen. Anstatt jedem eingehenden Request einen Thread zuzuweisen, verwendet es einen Hauptthread, um alle Requests zu verarbeiten und dann I/O-Operationen asynchron zu verarbeiten, wodurch der Overhead und die Komplexität des Erstellens, Zerstörens von Threads und des Umschaltens zwischen Threads vermieden werden.
Event Loop
Node.js verwaltet eine Ereigniswarteschlange im Hauptthread. Wenn ein Request empfangen wird, wird er dieser Warteschlange als Ereignis hinzugefügt, und dann werden weiterhin andere Requests empfangen. Wenn der Hauptthread im Leerlauf ist (keine Requests eingehen), beginnt er, die Ereigniswarteschlange zu durchlaufen, um zu überprüfen, ob Ereignisse zu verarbeiten sind. Es gibt zwei Fälle: Bei Nicht-I/O-Aufgaben verarbeitet der Hauptthread diese direkt und kehrt über eine Callback-Funktion zur oberen Ebene zurück; Bei I/O-Aufgaben nimmt er einen Thread aus dem Thread-Pool, um das Ereignis zu verarbeiten, gibt eine Callback-Funktion an und durchläuft dann weiterhin andere Ereignisse in der Warteschlange.
Sobald die I/O-Aufgabe im Thread abgeschlossen ist, wird die angegebene Callback-Funktion ausgeführt, und das abgeschlossene Ereignis wird am Ende der Ereigniswarteschlange platziert und wartet auf die Ereignisschleife. Wenn der Hauptthread dieses Ereignis erneut durchläuft, verarbeitet er es direkt und gibt es an die obere Ebene zurück. Dieser Prozess wird als Event Loop bezeichnet, und sein Funktionsprinzip ist in der folgenden Abbildung dargestellt:
Diese Abbildung zeigt das allgemeine Funktionsprinzip von Node.js. Von links nach rechts und von oben nach unten ist Node.js in vier Schichten unterteilt: die Anwendungsschicht, die V8-Engine-Schicht, die Node-API-Schicht und die LIBUV-Schicht.
- Anwendungsschicht: Es ist die JavaScript-Interaktionsschicht. Häufige Beispiele sind Node.js-Module wie
http
undfs
. - V8-Engine-Schicht: Sie verwendet die V8-Engine, um die JavaScript-Syntax zu parsen und dann mit den APIs der unteren Schicht zu interagieren.
- Node-API-Schicht: Sie bietet Systemaufrufe für die Module der oberen Schicht, die normalerweise in C implementiert sind, und interagiert mit dem Betriebssystem.
- LIBUV-Schicht: Es handelt sich um eine plattformübergreifende, zugrunde liegende Kapselung, die Ereignisschleifen, Dateivorgänge usw. realisiert und den Kern von Node.js für das Erreichen von Asynchronität darstellt.
Ob auf der Linux-Plattform oder der Windows-Plattform, Node.js verwendet intern den Thread-Pool, um asynchrone I/O-Operationen abzuschließen, und LIBUV vereinheitlicht die Aufrufe für unterschiedliche Plattformunterschiede. Der einzelne Thread in Node.js bedeutet also nur, dass JavaScript in einem einzelnen Thread ausgeführt wird, nicht, dass Node.js als Ganzes Single-Threaded ist.
Arbeitsprinzip
Der Kern von Node.js, um Asynchronität zu erreichen, liegt in den Ereignissen. Das heißt, es behandelt jede Aufgabe als ein Ereignis und simuliert dann den asynchronen Effekt durch den Event Loop. Um diese Tatsache konkreter und klarer zu verstehen und zu akzeptieren, verwenden wir Pseudocode, um ihr Funktionsprinzip unten zu beschreiben.
1. Definieren Sie die Ereigniswarteschlange
Da es sich um eine Warteschlange handelt, handelt es sich um eine FIFO-Datenstruktur (First-In, First-Out). Wir verwenden ein JS-Array, um es wie folgt zu beschreiben:
/** * Definieren Sie die Ereigniswarteschlange * Enqueue: push() * Dequeue: shift() * Leere Warteschlange: length === 0 */ let globalEventQueue = [];
Wir verwenden das Array, um die Warteschlangenstruktur zu simulieren: Das erste Element des Arrays ist der Kopf der Warteschlange und das letzte Element ist das Ende. push()
fügt ein Element am Ende der Warteschlange ein, und shift()
entfernt ein Element vom Kopf der Warteschlange. Somit wird eine einfache Ereigniswarteschlange erreicht.
2. Definieren Sie den Request-Empfangseingang
Jeder Request wird abgefangen und gelangt in die Verarbeitungsfunktion, wie unten gezeigt:
/** * Empfangen Sie Benutzeranfragen * Jeder Request gelangt in diese Funktion * Übergeben Sie die Parameter Request und Response */ function processHttpRequest(request, response) { // Definieren Sie ein Ereignisobjekt let event = createEvent({ params: request.params, // Übergeben Sie Request-Parameter result: null, // Speichern Sie Request-Ergebnisse callback: function() {} // Geben Sie eine Callback-Funktion an }); // Fügen Sie das Ereignis am Ende der Warteschlange hinzu globalEventQueue.push(event); }
Diese Funktion packt einfach den Request des Benutzers als Ereignis und fügt ihn in die Warteschlange ein, und empfängt dann weiterhin andere Requests.
3. Definieren Sie den Event Loop
Wenn sich der Hauptthread im Leerlauf befindet, beginnt er, die Ereigniswarteschlange zu durchlaufen. Wir müssen also eine Funktion definieren, um die Ereigniswarteschlange zu durchlaufen:
/** * Der Hauptteil der Ereignisschleife, der vom Hauptthread bei Bedarf ausgeführt wird * Durchlaufen Sie die Ereigniswarteschlange * Behandeln Sie Nicht-IO-Aufgaben * Behandeln Sie IO-Aufgaben * Führen Sie Callbacks aus und kehren Sie zur oberen Ebene zurück */ function eventLoop() { // Wenn die Warteschlange nicht leer ist, fahren Sie mit der Schleife fort while (this.globalEventQueue.length > 0) { // Nehmen Sie ein Ereignis vom Kopf der Warteschlange let event = this.globalEventQueue.shift(); // Wenn es sich um eine zeitaufwändige Aufgabe handelt if (isIOTask(event)) { // Nehmen Sie einen Thread aus dem Thread-Pool let thread = getThreadFromThreadPool(); // Übergeben Sie ihn an den Thread zur Verarbeitung thread.handleIOTask(event); } else { // Geben Sie nach der Bearbeitung von nicht zeitaufwändigen Aufgaben das Ergebnis direkt zurück let result = handleEvent(event); // Geben Sie schließlich über die Callback-Funktion an V8 zurück, und dann gibt V8 an die Anwendung zurück event.callback.call(null, result); } } }
Der Hauptthread überwacht kontinuierlich die Ereigniswarteschlange. Bei I/O-Aufgaben übergibt er sie zur Verarbeitung an den Thread-Pool, und bei Nicht-I/O-Aufgaben verarbeitet er sie selbst und gibt sie zurück.
4. Behandeln Sie I/O-Aufgaben
Nachdem der Thread-Pool die Aufgabe empfangen hat, verarbeitet er direkt die I/O-Operation, z. B. das Lesen der Datenbank:
/** * Behandeln Sie IO-Aufgaben * Fügen Sie das Ereignis nach Abschluss am Ende der Warteschlange hinzu * Geben Sie den Thread frei */ function handleIOTask(event) { // Aktueller Thread let curThread = this; // Bedienen Sie die Datenbank let optDatabase = function (params, callback) { let result = readDataFromDb(params); callback.call(null, result); }; // Führen Sie die IO-Aufgabe aus optDatabase(event.params, function (result) { // Speichern Sie das Rückergebnis im Ereignisobjekt event.result = result; // Nachdem die IO abgeschlossen ist, ist es keine zeitaufwändige Aufgabe mehr event.isIOTask = false; // Fügen Sie dieses Ereignis erneut am Ende der Warteschlange hinzu this.globalEventQueue.push(event); // Geben Sie den aktuellen Thread frei releaseThread(curThread); }); }
Wenn die I/O-Aufgabe abgeschlossen ist, wird der Callback ausgeführt, das Request-Ergebnis im Ereignis gespeichert und das Ereignis zurück in die Warteschlange gelegt, um auf die Schleife zu warten. Schließlich wird der aktuelle Thread freigegeben. Wenn der Hauptthread dieses Ereignis erneut durchläuft, verarbeitet er es direkt.
Zusammenfassend stellen wir fest, dass Node.js nur einen Hauptthread verwendet, um Requests zu empfangen. Nach dem Empfangen von Requests verarbeitet er sie nicht direkt, sondern legt sie in die Ereigniswarteschlange und empfängt dann weiterhin andere Requests. Wenn er sich im Leerlauf befindet, verarbeitet er diese Ereignisse über den Event Loop und erzielt so den asynchronen Effekt. Für I/O-Aufgaben muss er sich natürlich immer noch auf den Thread-Pool auf Systemebene verlassen, um sie zu verarbeiten.
Daher können wir einfach verstehen, dass Node.js selbst eine Multi-Threaded-Plattform ist, aber Aufgaben auf JavaScript-Ebene in einem Single-Thread verarbeitet.
CPU-intensive Aufgaben sind eine Schwäche
Inzwischen sollten wir ein einfaches und klares Verständnis des Single-Thread-Modells von Node.js haben. Es erreicht hohe Gleichzeitigkeit und asynchrone I/O durch das ereignisgesteuerte Modell. Es gibt jedoch auch Dinge, in denen Node.js nicht gut ist.
Wie oben erwähnt, übergibt Node.js für I/O-Aufgaben diese zur asynchronen Verarbeitung an den Thread-Pool, was effizient und einfach ist. Node.js eignet sich also für die Bearbeitung von I/O-intensiven Aufgaben. Aber nicht alle Aufgaben sind I/O-intensiv. Bei CPU-intensiven Aufgaben, d. h. Operationen, die nur auf CPU-Berechnungen basieren, wie z. B. Datenverschlüsselung und -entschlüsselung (node.bcrypt.js
), Datenkomprimierung und -dekomprimierung (node-tar
), verarbeitet Node.js diese einzeln. Wenn die vorherige Aufgabe nicht abgeschlossen ist, können die nachfolgenden Aufgaben nur warten. Wie in der folgenden Abbildung gezeigt:
In der Ereigniswarteschlange werden nachfolgende Aufgaben blockiert, wenn die vorherigen CPU-Berechnungsaufgaben nicht abgeschlossen sind, was zu einer langsamen Reaktion führt. Wenn das Betriebssystem Single-Core ist, ist es möglicherweise tolerierbar. Aber jetzt sind die meisten Server Multi-CPU oder Multi-Core, und Node.js hat nur einen EventLoop, was bedeutet, dass er nur einen CPU-Kern belegt. Wenn Node.js von CPU-intensiven Aufgaben belegt ist, wodurch andere Aufgaben blockiert werden, sind immer noch CPU-Kerne im Leerlauf, was zu einer Ressourcenverschwendung führt.
Node.js ist also nicht für CPU-intensive Aufgaben geeignet.
Anwendungsszenarien
- RESTful API: Requests und Responses erfordern nur eine kleine Menge an Text und benötigen nicht viel logische Verarbeitung. Daher können Zehntausende von Verbindungen gleichzeitig verarbeitet werden.
- Chat-Dienst: Er ist leichtgewichtig, hat hohes Traffic und keine komplexen Berechnungslogiken.
Leapcell: Die Serverless-Plattform der nächsten Generation für Webhosting, asynchrone Aufgaben und Redis
Schließlich möchte ich die Plattform vorstellen, die sich am besten für die Bereitstellung von Node.js-Diensten eignet: Leapcell.
1. Multi-Sprachen-Unterstützung
- Entwickeln Sie mit JavaScript, Python, Go oder Rust.
2. Stellen Sie unbegrenzt Projekte kostenlos bereit
- Zahlen Sie nur für die Nutzung – keine Requests, keine Gebühren.
3. Unschlagbare Kosteneffizienz
- Pay-as-you-go ohne Leerlaufgebühren.
- Beispiel: 25 $ unterstützen 6,94 Millionen Requests bei einer durchschnittlichen Antwortzeit von 60 ms.
4. Optimierte Entwicklererfahrung
- Intuitive Benutzeroberfläche für mühelose Einrichtung.
- Vollautomatische CI/CD-Pipelines und GitOps-Integration.
- Echtzeitmetriken und -protokollierung für umsetzbare Einblicke.
5. Mühelose Skalierbarkeit und hohe Leistung
- Auto-Skalierung zur einfachen Bewältigung hoher Gleichzeitigkeit.
- Null Betriebsaufwand – konzentrieren Sie sich einfach auf das Bauen.
Erfahren Sie mehr in der Dokumentation!
Leapcell Twitter: https://x.com/LeapcellHQ