Multi-Threading in Node.js
Min-jun Kim
Dev Intern · Leapcell

In Node.js wird aufgrund seiner Single-Threaded-Natur der Hauptthread verwendet, um nicht-blockierende I/O-Operationen auszuführen. Wenn jedoch CPU-intensive Aufgaben ausgeführt werden, kann die alleinige Verwendung eines einzelnen Threads zu Performance-Engpässen führen. Glücklicherweise bietet Node.js mehrere Methoden, um Threads zu aktivieren und zu verwalten, sodass Anwendungen die Vorteile von Multi-Core-CPUs nutzen können.
Warum Subthreads aktivieren?
Die Hauptgründe für die Aktivierung von Subthreads in Node.js sind die Bearbeitung gleichzeitiger Aufgaben und die Verbesserung der Anwendungsleistung. Node.js basiert von Natur aus auf einem Event-Loop-Single-Threaded-Modell, was bedeutet, dass alle I/O-Operationen (wie z. B. das Lesen/Schreiben von Dateien und Netzwerkanfragen) nicht-blockierend sind. CPU-intensive Aufgaben (wie z. B. umfangreiche Berechnungen) können jedoch die Event-Loop blockieren und die Gesamtleistung der Anwendung beeinträchtigen.
Die Aktivierung von Subthreads hilft bei der Lösung der folgenden Probleme:
- Nicht-blockierende Operationen: Die Designphilosophie von Node.js dreht sich um nicht-blockierende I/O. Wenn jedoch externe Befehle direkt im Hauptthread ausgeführt werden, kann der Ausführungsprozess den Hauptthread blockieren und die Reaktionsfähigkeit der Anwendung beeinträchtigen. Durch die Ausführung dieser Befehle in Subthreads behält der Hauptthread seine nicht-blockierenden Eigenschaften bei und stellt sicher, dass andere gleichzeitige Operationen nicht beeinträchtigt werden.
- Effiziente Nutzung von Systemressourcen: Durch die Verwendung von Child-Prozessen oder Worker-Threads kann eine Node.js-Anwendung die Rechenleistung von Multi-Core-CPUs besser nutzen. Dies ist besonders nützlich für die Ausführung CPU-intensiver externer Befehle, da diese auf separaten CPU-Kernen ausgeführt werden können, ohne Node.js’s Haupt-Event-Loop zu beeinträchtigen.
- Isolation und Sicherheit: Die Ausführung externer Befehle in Subthreads fügt der Anwendung eine zusätzliche Sicherheitsebene hinzu. Wenn ein externer Befehl fehlschlägt oder abstürzt, hilft diese Isolation, den Hauptprozess von Node.js vor Beeinträchtigungen zu schützen, wodurch die Anwendungsstabilität verbessert wird.
- Flexible Datenverarbeitung und Kommunikation: Mit Subthreads können externe Befehlsausgaben flexibel verarbeitet werden, bevor sie an den Hauptprozess zurückgegeben werden. Node.js bietet mehrere Möglichkeiten zur Implementierung der Interprozesskommunikation (IPC), wodurch der Datenaustausch nahtlos erfolgt.
Methoden zur Aktivierung von Subthreads
Als Nächstes werden wir verschiedene Möglichkeiten zur Aktivierung von Subthreads in Node.js untersuchen.
Child-Prozesse
Das child_process
-Modul von Node.js ermöglicht die Ausführung von Systembefehlen oder anderen Programmen durch die Erstellung von Child-Prozessen, die mit dem Hauptprozess kommunizieren können. Dies ist nützlich für die Ausführung CPU-intensiver Aufgaben oder die Ausführung anderer Anwendungen.
spawn()
Die spawn()
-Methode im child_process
-Modul wird verwendet, um einen neuen Child-Prozess zu erstellen, der einen bestimmten Befehl ausführt. Sie gibt ein Objekt mit stdout
- und stderr
-Streams zurück, das die Interaktion mit dem Child-Prozess ermöglicht. Diese Methode ist ideal für lang laufende Prozesse, die große Ausgabemengen erzeugen, da sie Daten als Stream verarbeitet und nicht alles auf einmal puffert.
Die grundlegende Syntax der Funktion spawn()
lautet:
const { spawn } = require('child_process'); const child = spawn(command, [args], [options]);
command
: Eine Zeichenkette, die den auszuführenden Befehl darstellt.args
: Ein Array von Zeichenketten, das alle Befehlszeilenargumente auflistet.options
: Ein optionales Objekt, das konfiguriert, wie der Child-Prozess erstellt wird. Zu den gängigen Optionen gehören:cwd
: Das Arbeitsverzeichnis des Child-Prozesses.env
: Ein Objekt, das Umgebungsvariablen enthält.stdio
: Konfiguriert die Standardeingabe/-ausgabe des Child-Prozesses, die häufig für Pipe-Operationen oder Dateiumleitungen verwendet wird.shell
: Wenntrue
, führt den Befehl in einer Shell aus. Die Standardshell ist/bin/sh
unter Unix undcmd.exe
unter Windows.detached
: Wenntrue
, wird der Child-Prozess unabhängig vom Elternprozess ausgeführt und kann nach dem Beenden des Elternprozesses weiterlaufen.
Hier ist ein einfaches Beispiel für die Verwendung von spawn()
:
const { spawn } = require('child_process'); const path = require('path'); // Verwenden Sie den Befehl 'touch', um eine Datei namens 'moment.txt' zu erstellen const touch = spawn('touch', ['moment.txt'], { cwd: path.join(process.cwd(), './m'), }); touch.on('close', (code) => { if (code === 0) { console.log('Datei erfolgreich erstellt'); } else { console.error(`Fehler beim Erstellen der Datei, Exit-Code: ${code}`); } });
Der Zweck dieses Codes ist die Erstellung einer leeren Datei namens moment.txt
im Unterverzeichnis m
des aktuellen Arbeitsverzeichnisses. Bei Erfolg wird eine Erfolgsmeldung ausgegeben, andernfalls eine Fehlermeldung.
exec()
Die exec()
-Methode im child_process
-Modul wird verwendet, um einen neuen Child-Prozess zu erstellen, der einen bestimmten Befehl ausführt und alle erzeugten Ausgaben puffert. Im Gegensatz zu spawn()
eignet sich exec()
besser für Szenarien, in denen die Ausgabe gering ist, da es stdout
und stderr
des Child-Prozesses im Speicher speichert.
Die grundlegende Syntax von exec()
lautet:
const { exec } = require('child_process'); exec(command, [options], callback);
command
: Der auszuführende Befehl als Zeichenkette.options
: Optionale Parameter zur Anpassung der Ausführungsumgebung.callback
: Eine Callback-Funktion, die(error, stdout, stderr)
als Argumente empfängt.
Das options
-Objekt kann Folgendes enthalten:
cwd
: Legt das Arbeitsverzeichnis des Child-Prozesses fest.env
: Gibt ein Umgebungsvariablenobjekt an.encoding
: Die Zeichenkodierung.shell
: Gibt die für die Ausführung verwendete Shell an (/bin/sh
unter Unix,cmd.exe
unter Windows).timeout
: Legt einen Timeout in Millisekunden fest; der Child-Prozess wird beendet, wenn die Ausführung diese Zeit überschreitet.maxBuffer
: Legt die maximale Puffergröße fürstdout
undstderr
fest (Standard:1024 * 1024
oder 1 MB).killSignal
: Definiert das Signal, das zum Beenden des Prozesses verwendet wird (Standard:'SIGTERM'
).
Die Callback-Funktion empfängt:
error
: EinError
-Objekt, wenn die Befehlsausführung fehlschlägt oder einen Exit-Code ungleich Null zurückgibt; andernfallsnull
.stdout
: Die Standardausgabe des Befehls.stderr
: Die Standardfehlerausgabe.
Hier ist ein Beispiel für die Verwendung von exec()
:
const { exec } = require('child_process'); const path = require('path'); // Definieren Sie den auszuführenden Befehl, einschließlich des Dateipfads const command = `touch ${path.join('./m', 'moment.txt')}`; exec(command, { cwd: process.cwd() }, (error, stdout, stderr) => { if (error) { console.error(`Fehler beim Ausführen des Befehls: ${error}`); return; } if (stderr) { console.error(`Standardfehlerausgabe: ${stderr}`); return; } console.log('Datei erfolgreich erstellt'); });
Die Ausführung dieses Codes erstellt die Datei und zeigt die entsprechende Ausgabe an.
fork()
Die fork()
-Methode im child_process
-Modul ist eine spezielle Möglichkeit, einen neuen Node.js-Prozess zu erstellen, der über einen Interprozesskommunikationskanal (IPC) mit dem Elternprozess kommuniziert. fork()
ist besonders nützlich, wenn Node.js-Module separat ausgeführt werden, und ist vorteilhaft für die parallele Ausführung auf Multi-Core-CPUs.
Die grundlegende Syntax von fork()
lautet:
const { fork } = require('child_process'); const child = fork(modulePath, [args], [options]);
modulePath
: Eine Zeichenkette, die den Pfad des Moduls darstellt, das im Child-Prozess ausgeführt werden soll.args
: Ein Array von Zeichenketten, das Argumente enthält, die an das Modul übergeben werden sollen.options
: Ein optionales Objekt zur Konfiguration des Child-Prozesses.
Das Objekt options
kann Folgendes enthalten:
cwd
: Das Arbeitsverzeichnis des Child-Prozesses.env
: Ein Objekt, das Umgebungsvariablen enthält.execPath
: Der Pfad zur ausführbaren Node.js-Datei, die zum Erstellen des Child-Prozesses verwendet wird.execArgv
: Eine Liste von Argumenten, die an die ausführbare Node.js-Datei, aber nicht an das Modul selbst übergeben werden.silent
: Beitrue
leitet esstdin
,stdout
undstderr
des Child-Prozesses an den Elternprozess um; andernfalls erben sie vom Elternprozess.stdio
: Konfiguriert die Standardeingabe- und Ausgabestreams.ipc
: Erstellt einen IPC-Kanal für die Kommunikation zwischen dem Eltern- und dem Child-Prozess.
Ein mit fork()
erstellter Child-Prozess richtet automatisch einen IPC-Kanal ein, der die Nachrichtenübertragung zwischen dem Eltern- und dem Child-Prozess ermöglicht. Der Elternprozess kann Nachrichten mit child.send(message)
senden, und der Child-Prozess kann mit process.on('message', callback)
auf diese Nachrichten warten. Ebenso kann der Child-Prozess mit process.send(message)
Nachrichten an den Elternprozess senden.
Hier ist ein Beispiel, das die Verwendung von fork()
zum Erstellen eines Child-Prozesses und zur Kommunikation über IPC demonstriert:
index.js
(Elternprozess)
const { fork } = require('child_process'); const child = fork('./child.js'); child.on('message', (message) => { console.log('Nachricht vom Child-Prozess:', message); }); child.send({ hello: 'world' }); setInterval(() => { child.send({ hello: 'world' }); }, 1000);
child.js
(Child-Prozess)
process.on('message', (message) => { console.log('Nachricht vom Elternprozess:', message); }); process.send({ foo: 'bar' }); setInterval(() => { process.send({ hello: 'world' }); }, 1000);
In diesem Beispiel erstellt der Elternprozess (index.js
) einen Child-Prozess, der child.js
ausführt. Der Elternprozess sendet eine Nachricht an das Kind, die diese empfängt und protokolliert und dann eine Antwort zurücksendet. Der Elternprozess protokolliert auch Nachrichten, die er vom Kind empfängt. Ein Timer sorgt für einen regelmäßigen Nachrichtenaustausch.
Mit fork()
wird jeder Child-Prozess als separate Node.js-Instanz mit eigener V8-Engine und eigenem Event-Loop ausgeführt. Dies bedeutet, dass die Erstellung von zu vielen Child-Prozessen zu einem hohen Ressourcenverbrauch führen kann.
Worker-Threads
Das worker_threads
-Modul in Node.js bietet einen Mechanismus zum parallelen Ausführen mehrerer JavaScript-Aufgaben innerhalb eines einzigen Prozesses. Dies ermöglicht es Anwendungen, die Multi-Core-CPU-Ressourcen voll auszuschöpfen, insbesondere für CPU-intensive Aufgaben, ohne mehrere Prozesse zu erzeugen. Die Verwendung von worker_threads
kann die Leistung erheblich verbessern und komplexe Berechnungen ermöglichen.
Wichtige Konzepte von Worker-Threads:
- Worker: Ein unabhängiger Thread, der JavaScript-Code ausführt. Jeder Worker läuft in seiner eigenen V8-Instanz, hat seinen eigenen Event-Loop und lokale Variablen, was bedeutet, dass er unabhängig vom Hauptthread oder anderen Workern arbeiten kann.
- Hauptthread: Der Thread, der einen Worker initiiert. In einer typischen Node.js-Anwendung läuft die anfängliche JavaScript-Ausführungsumgebung (der Event-Loop) im Hauptthread.
- Kommunikation: Der Hauptthread und die Worker kommunizieren durch das Senden von Nachrichten. Sie können JavaScript-Werte senden, einschließlich
ArrayBuffer
und anderer übertragbarer Objekte, was einen effizienten Datentransfer ermöglicht.
Hier ist ein einfaches Beispiel, das die Erstellung eines Workers und die Kommunikation zwischen dem Hauptthread und dem Worker demonstriert:
const { Worker, isMainThread, parentPort } = require('worker_threads'); if (isMainThread) { // Hauptthread const worker = new Worker(__filename); worker.on('message', (message) => { console.log('Nachricht vom Worker:', message); }); worker.postMessage('Hallo Worker!'); } else { // Worker-Thread parentPort.on('message', (message) => { console.log('Nachricht vom Hauptthread:', message); parentPort.postMessage('Hallo Hauptthread!'); }); }
In diesem Beispiel dient die Datei index.js
sowohl als Einstiegspunkt für den Hauptthread als auch als Worker-Skript. Durch die Überprüfung von isMainThread
ermittelt das Skript, ob es im Hauptthread oder als Worker ausgeführt wird. Der Hauptthread erstellt einen Worker, der dasselbe Skript ausführt, und sendet dann eine Nachricht an den Worker. Der Worker antwortet über postMessage()
.
Unterschiede zwischen worker_threads
und fork()
Konzept:
worker_threads
: Verwendet Worker-Threads, um JavaScript-Code parallel innerhalb desselben Prozesses auszuführen.fork()
: Erzeugt einen separaten Node.js-Prozess mit eigener V8-Instanz und eigenem Event-Loop.
Kommunikation:
worker_threads
: VerwendetMessagePort
, um JavaScript-Werte zu übertragen, einschließlichArrayBuffer
undMessageChannel
.fork()
: Verwendet IPC (Interprozesskommunikation) überprocess.send()
- undmessage
-Ereignisse.
Speichernutzung:
worker_threads
: Verwendet gemeinsam genutzten Speicher, wodurch redundante Datenkopien reduziert werden, was zu einer besseren Leistung führt.fork()
: Jeder geforkte Prozess hat einen separaten Speicherbereich und eine eigene V8-Instanz, was zu einem höheren Speicherverbrauch führt.
Beste Anwendungsfälle:
worker_threads
: Geeignet für CPU-intensive Berechnungen und Parallelverarbeitung.fork()
: Geeignet für die Ausführung unabhängiger Node.js-Anwendungen oder isolierter Dienste.
Insgesamt hängt die Wahl zwischen worker_threads
und fork()
von den Anforderungen Ihrer Anwendung ab. Wenn Sie eine strikte Prozessisolation benötigen, ist fork()
möglicherweise die bessere Option. Wenn Sie jedoch eine effiziente parallele Berechnung und Datenverarbeitung benötigen, bietet worker_threads
eine bessere Leistung und Ressourcenauslastung.
Cluster (Clustering)
Das cluster
-Modul in Node.js ermöglicht die Erstellung von Child-Prozessen, die denselben Serverport gemeinsam nutzen. Dies ermöglicht es einer Node.js-Anwendung, auf mehreren CPU-Kernen zu laufen, was die Leistung und den Durchsatz verbessert. Da Node.js Single-Threaded ist, funktionieren seine nicht-blockierenden I/O-Operationen gut für die Verarbeitung vieler gleichzeitiger Verbindungen. Für CPU-intensive Aufgaben oder bei der Verteilung der Arbeitslast auf mehrere Kerne ist die Verwendung des cluster
-Moduls jedoch besonders nützlich.
Das grundlegende Funktionsprinzip des cluster
-Moduls besteht darin, dass es einem Masterprozess (oft als "Master" bezeichnet) ermöglicht, mehrere Worker-Prozesse zu erstellen, die im Wesentlichen Kopien des Hauptprozesses sind. Der Masterprozess verwaltet diese Worker und verteilt eingehende Netzwerkverbindungen auf diese.
Intern verwendet das cluster
-Modul child_process.fork()
, um Worker-Prozesse zu erstellen, was bedeutet, dass jeder Worker denselben Anwendungscode ausführt. Der Hauptunterschied besteht darin, dass sie über IPC (Interprozesskommunikation) mit dem Masterprozess kommunizieren können.
Hier ist ein einfaches Beispiel für die Verwendung des cluster
-Moduls:
const cluster = require('cluster'); const http = require('http'); const numCPUs = require('os').cpus().length; if (cluster.isMaster) { console.log(`Masterprozess ${process.pid} wird ausgeführt`); // Fork Worker-Prozesse for (let i = 0; i < numCPUs; i++) { cluster.fork(); } cluster.on('exit', (worker, code, signal) => { console.log(`Worker-Prozess ${worker.process.pid} beendet`); }); } else { // Worker-Prozesse können jede TCP-Verbindung gemeinsam nutzen // In diesem Beispiel erstellen sie einen HTTP-Server http .createServer((req, res) => { res.writeHead(200); res.end('hallo welt\n'); }) .listen(8000); console.log(`Worker-Prozess ${process.pid} gestartet`); }
Wenn Sie dieses Skript ausführen und Anfragen an den Server senden, werden in den Protokollen unterschiedliche Prozess-IDs angezeigt, was darauf hindeutet, dass mehrere Worker-Prozesse Anfragen bearbeiten.
In diesem Beispiel erstellt der Masterprozess Worker-Prozesse, die der Anzahl der CPU-Kerne entsprechen. Jeder Worker-Prozess läuft unabhängig und bearbeitet eingehende HTTP-Anfragen. Wenn ein Worker-Prozess beendet wird, wird der Masterprozess über das Ereignis exit
benachrichtigt.
Während das cluster
-Modul die Leistung und Zuverlässigkeit verbessert, erhöht es auch die Komplexität, z. B. die Verwaltung von Worker-Lebenszyklen und die Handhabung der Interprozesskommunikation. In einigen Fällen sind alternative Lösungen wie die Verwendung eines Prozessmanagers (z. B. pm2
) möglicherweise besser geeignet.
Das cluster
-Modul ist jedoch nicht für alle Anwendungen erforderlich. Für nicht CPU-intensive Anwendungen kann eine einzelne Node.js-Instanz ausreichen, um alle Workloads zu bewältigen.
Zusammenfassung
Child-Prozesse ermöglichen es Node.js-Anwendungen, Betriebssystembefehle auszuführen oder unabhängige Node.js-Module auszuführen, wodurch die Gleichzeitigkeitsverarbeitung verbessert wird. Mithilfe von APIs wie exec()
, spawn()
und fork()
können Entwickler auf flexible Weise Child-Prozesse erstellen und verwalten, wodurch komplexe asynchrone und nicht-blockierende Operationen ermöglicht werden. Dies ermöglicht es Anwendungen, die Systemressourcen und die Vorteile von Multi-Core-CPUs voll auszuschöpfen, ohne den Haupt-Event-Loop zu beeinträchtigen.
Indem Sie die passende Threading-Methode wählen – ob Child-Prozesse, Worker-Threads oder Clustering – können Sie Ihre Node.js-Anwendung sowohl in Bezug auf Leistung als auch auf Skalierbarkeit optimieren.
Wir sind Leapcell, Ihre erste Wahl für das Hosting von Node.js-Projekten.
Leapcell ist die Serverless-Plattform der nächsten Generation für Webhosting, asynchrone Aufgaben und Redis:
Multi-Sprachen Unterstützung
- Entwickeln Sie mit Node.js, Python, Go oder Rust.
Unbegrenzte Projekte kostenlos bereitstellen
- Zahlen Sie nur für die Nutzung - keine Anfragen, keine Gebühren.
Unschlagbare Kosteneffizienz
- Pay-as-you-go ohne Leerlaufgebühren.
- Beispiel: 25 US-Dollar unterstützen 6,94 Millionen Anfragen bei einer durchschnittlichen Reaktionszeit von 60 ms.
Optimierte Entwicklererfahrung
- Intuitive Benutzeroberfläche für mühelose Einrichtung.
- Vollautomatisierte CI/CD-Pipelines und GitOps-Integration.
- Echtzeitmetriken und Protokollierung für umsetzbare Informationen.
Mühelose Skalierbarkeit und hohe Leistung
- Automatische Skalierung zur einfachen Bewältigung hoher Parallelität.
- Kein Betriebsaufwand – konzentrieren Sie sich einfach auf den Aufbau.
Erfahren Sie mehr in der Dokumentation!
Folgen Sie uns auf X: @LeapcellHQ