Der Autor hat Open Sourcing Mental Illness ausgewählt, um eine Spende im Rahmen des Write for DOnations-Programms zu erhalten.
Einführung
Node.js führt JavaScript-Code in einem einzigen Thread aus, was bedeutet, dass Ihr Code nur eine Aufgabe gleichzeitig ausführen kann. Node.js selbst ist jedoch mehrfädig und bietet verborgene Threads über die libuv
-Bibliothek, die Ein- und Ausgabeoperationen wie das Lesen von Dateien von einer Festplatte oder Netzwerkanfragen behandelt. Durch die Verwendung verborgener Threads stellt Node.js asynchrone Methoden bereit, die es Ihrem Code ermöglichen, Ein-/Ausgabe-Anforderungen zu stellen, ohne den Hauptthread zu blockieren.
Auch wenn Node.js verborgene Threads hat, können Sie diese nicht verwenden, um rechenintensive Aufgaben wie komplexe Berechnungen, Bildgrößenänderungen oder Videokomprimierung auszulagern. Da JavaScript ein Single-Thread-Modell verwendet, blockiert eine rechenintensive Aufgabe den Hauptthread und kein anderer Code wird ausgeführt, bis die Aufgabe abgeschlossen ist. Ohne die Verwendung anderer Threads ist der einzige Weg, um eine CPU-intensive Aufgabe zu beschleunigen, die Erhöhung der Prozessorgeschwindigkeit.
Jedoch in den letzten Jahren werden CPUs nicht schneller. Stattdessen werden Computer mit zusätzlichen Kernen ausgeliefert, und es ist nun üblicher, dass Computer 8 oder mehr Kerne haben. Trotz dieses Trends wird Ihr Code die zusätzlichen Kerne Ihres Computers nicht nutzen, um CPU-lastige Aufgaben zu beschleunigen oder das Hauptprogramm nicht zu blockieren, da JavaScript ein Einzelthread ist.
Um dies zu beheben, hat Node.js das worker-threads
-Modul eingeführt, mit dem Sie Threads erstellen und mehrere JavaScript-Aufgaben parallel ausführen können. Sobald ein Thread eine Aufgabe beendet hat, sendet er eine Nachricht an den Hauptthread, die das Ergebnis der Operation enthält, sodass es mit anderen Teilen des Codes verwendet werden kann. Der Vorteil der Verwendung von Worker-Threads besteht darin, dass CPU-lastige Aufgaben den Hauptthread nicht blockieren und Sie eine Aufgabe auf mehrere Worker aufteilen und verteilen können, um sie zu optimieren.
In diesem Tutorial erstellen Sie eine Node.js-App mit einer CPU-intensiven Aufgabe, die den Hauptthread blockiert. Anschließend verwenden Sie das worker-threads
-Modul, um die CPU-intensive Aufgabe in einen anderen Thread auszulagern, um den Hauptthread nicht zu blockieren. Schließlich teilen Sie die CPU-lastige Aufgabe auf und lassen vier Threads parallel daran arbeiten, um die Aufgabe zu beschleunigen.
Voraussetzungen
Um dieses Tutorial abzuschließen, benötigen Sie:
-
Ein Mehrkernsystem mit vier oder mehr Kernen. Sie können dem Tutorial auf einem Dual-Core-System weiterhin Schritte 1 bis 6 folgen. Schritt 7 erfordert jedoch vier Kerne, um die Leistungsverbesserungen zu sehen.
-
Eine Node.js-Entwicklungsumgebung. Wenn Sie Ubuntu 22.04 verwenden, installieren Sie die neueste Version von Node.js, indem Sie Schritt 3 von So installieren Sie Node.js unter Ubuntu 22.04 befolgen. Wenn Sie ein anderes Betriebssystem verwenden, sehen Sie So installieren Sie Node.js und erstellen eine lokale Entwicklungsumgebung.
-
Ein gutes Verständnis der Ereignisschleife, Rückrufe und Versprechen in JavaScript, das Sie in unserem Tutorial finden können, Verständnis der Ereignisschleife, Rückrufe, Versprechen und Async/Await in JavaScript.
-
Grundkenntnisse darüber, wie man das Express-Webframework verwendet. Schauen Sie sich unseren Leitfaden an, So starten Sie mit Node.js und Express.
Einrichten des Projekts und Installieren von Abhängigkeiten
In diesem Schritt erstellen Sie das Projektverzeichnis, initialisieren npm
und installieren alle notwendigen Abhängigkeiten.
Beginnen Sie damit, das Projektverzeichnis zu erstellen und in dieses zu wechseln:
- mkdir multi-threading_demo
- cd multi-threading_demo
Der Befehl mkdir
erstellt ein Verzeichnis, und der Befehl cd
ändert das Arbeitsverzeichnis in das neu erstellte.
Anschließend initialisieren Sie das Projektverzeichnis mit npm mithilfe des Befehls npm init
:
- npm init -y
Die Option -y
akzeptiert alle Standardoptionen.
Wenn der Befehl ausgeführt wird, sieht Ihre Ausgabe ähnlich aus wie folgt:
Wrote to /home/sammy/multi-threading_demo/package.json:
{
"name": "multi-threading_demo",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC"
}
Als nächstes installieren Sie express
, ein Node.js-Webframework:
- npm install express
Sie werden Express verwenden, um eine Serveranwendung zu erstellen, die blockierende und nicht blockierende Endpunkte hat.
Node.js wird standardmäßig mit dem Modul worker-threads
geliefert, daher müssen Sie es nicht installieren.
Sie haben jetzt die erforderlichen Pakete installiert. Als nächstes erfahren Sie mehr über Prozesse und Threads und wie sie auf einem Computer ausgeführt werden.
Verständnis von Prozessen und Threads
Bevor Sie damit beginnen, CPU-gebundene Aufgaben zu schreiben und sie auf separate Threads auszulagern, müssen Sie zunächst verstehen, was Prozesse und Threads sind und welche Unterschiede es zwischen ihnen gibt. Am wichtigsten ist, dass Sie überprüfen, wie Prozesse und Threads auf einem Einzel- oder Mehrkern-Computersystem ausgeführt werden.
Prozess
A process is a running program in the operating system. It has its own memory and cannot see nor access the memory of other running programs. It also has an instruction pointer, which indicates the instruction currently being executed in a program. Only one task can be executed at a time.
Um dies zu verstehen, werden Sie ein Node.js-Programm mit einer Endlosschleife erstellen, damit es beim Ausführen nicht beendet wird.
Verwenden Sie nano
oder Ihren bevorzugten Texteditor, um die Datei process.js
zu erstellen und zu öffnen:
- nano process.js
In Ihrer process.js
-Datei geben Sie den folgenden Code ein:
const process_name = process.argv.slice(2)[0];
count = 0;
while (true) {
count++;
if (count == 2000 || count == 4000) {
console.log(`${process_name}: ${count}`);
}
}
In der ersten Zeile gibt die Eigenschaft process.argv
ein Array zurück, das die Befehlszeilenargumente des Programms enthält. Sie hängen dann die JavaScript-Methode slice()
mit einem Argument von 2
an, um eine oberflächliche Kopie des Arrays ab Index 2 zu erstellen. Dadurch werden die ersten beiden Argumente übersprungen, die der Pfad von Node.js und der Dateiname des Programms sind. Anschließend verwenden Sie die Notationssyntax mit eckigen Klammern, um das erste Argument aus dem geschnittenen Array abzurufen und es in der Variablen process_name
zu speichern.
Nachdem das while
-Schleife definiert wurde und ihr eine true
-Bedingung übergeben wurde, um die Schleife endlos laufen zu lassen, wird innerhalb der Schleife die count
-Variable bei jeder Iteration um 1
erhöht. Danach folgt eine if
-Anweisung, die überprüft, ob count
gleich 2000
oder 4000
ist. Wenn die Bedingung wahr ist, gibt die console.log()
-Methode eine Nachricht in der Konsole aus.
Speichern und schließen Sie Ihre Datei mit CTRL+X
und drücken Sie dann Y
, um die Änderungen zu speichern.
Führen Sie das Programm mit dem node
-Befehl aus:
- node process.js A &
A
is a command-line argument that is passed to the program and stored in the process_name
variable. The &
at end the allows the Node program to run in the background, which lets you enter more commands in the shell.
Wenn Sie das Programm ausführen, sehen Sie eine Ausgabe ähnlich wie folgt:
Output[1] 7754
A: 2000
A: 4000
Die Zahl 7754
ist eine Prozess-ID, die vom Betriebssystem zugewiesen wurde. A: 2000
und A: 4000
sind die Ausgaben des Programms.
Wenn Sie ein Programm mit dem node
-Befehl ausführen, erstellen Sie einen Prozess. Das Betriebssystem reserviert Speicher für das Programm, lokalisiert die ausführbare Programmdatei auf der Festplatte Ihres Computers und lädt das Programm in den Speicher. Anschließend weist es ihm eine Prozess-ID zu und beginnt mit der Ausführung des Programms. Zu diesem Zeitpunkt ist Ihr Programm nun ein Prozess.
Wenn der Prozess läuft, wird seine Prozess-ID der Prozessliste des Betriebssystems hinzugefügt und kann mit Tools wie htop
, top
oder ps
eingesehen werden. Die Tools bieten weitere Details zu den Prozessen sowie Optionen, um sie zu stoppen oder zu priorisieren.
Um eine schnelle Zusammenfassung eines Node-Prozesses zu erhalten, drücken Sie ENTER
in Ihrem Terminal, um die Eingabeaufforderung zurückzuerhalten. Führen Sie dann den Befehl ps
aus, um die Node-Prozesse anzuzeigen:
- ps |grep node
Der Befehl ps
listet alle Prozesse des aktuellen Benutzers im System auf. Der Pipe-Operator |
überträgt alle Ausgaben von ps
an das grep
, um nur Node-Prozesse aufzulisten.
Die Ausführung des Befehls liefert eine Ausgabe ähnlich der folgenden:
Output7754 pts/0 00:21:49 node
Sie können unzählige Prozesse aus einem einzelnen Programm erstellen. Verwenden Sie beispielsweise den folgenden Befehl, um drei weitere Prozesse mit unterschiedlichen Argumenten zu erstellen und im Hintergrund auszuführen:
- node process.js B & node process.js C & node process.js D &
In dem Befehl haben Sie drei weitere Instanzen des Programms process.js
erstellt. Das Symbol &
platziert jeden Prozess im Hintergrund.
Nach Ausführung des Befehls sieht die Ausgabe ähnlich wie folgt aus (obwohl die Reihenfolge variieren kann):
Output[2] 7821
[3] 7822
[4] 7823
D: 2000
D: 4000
B: 2000
B: 4000
C: 2000
C: 4000
Wie Sie in der Ausgabe sehen können, hat jeder Prozess den Prozessnamen in das Terminal protokolliert, als die Anzahl 2000
und 4000
erreicht wurde. Jeder Prozess ist sich nicht bewusst, dass andere Prozesse ausgeführt werden: Prozess D
ist sich nicht bewusst, dass Prozess C
läuft, und umgekehrt. Alles, was in einem der Prozesse passiert, beeinflusst andere Node.js-Prozesse nicht.
Wenn Sie die Ausgabe genau untersuchen, werden Sie feststellen, dass die Reihenfolge der Ausgabe nicht dieselbe Reihenfolge ist, die Sie hatten, als Sie die drei Prozesse erstellt haben. Beim Ausführen des Befehls waren die Prozessargumente in der Reihenfolge von B
, C
und D
. Aber jetzt ist die Reihenfolge D
, B
und C
. Der Grund dafür ist, dass das Betriebssystem Planungsalgorithmen hat, die entscheiden, welcher Prozess zu einem bestimmten Zeitpunkt auf der CPU ausgeführt werden soll.
Auf einer Single-Core-Maschine führen die Prozesse parallel aus. Das bedeutet, dass das Betriebssystem regelmäßig zwischen den Prozessen wechselt. Zum Beispiel führt der Prozess D
für eine begrenzte Zeit aus, dann wird sein Zustand irgendwo gespeichert und das Betriebssystem plant den Prozess B
auszuführen, und so weiter. Dies geschieht hin und her, bis alle Aufgaben abgeschlossen sind. Aus der Ausgabe könnte es so aussehen, als ob jeder Prozess bis zum Abschluss ausgeführt wurde, aber in Wirklichkeit wechselt der OS-Scheduler ständig zwischen ihnen hin und her.
Auf einem Multi-Core-System – vorausgesetzt, Sie haben vier Kerne – plant das Betriebssystem jeden Prozess gleichzeitig auf jedem Kern auszuführen. Dies wird als Parallelität bezeichnet. Wenn Sie jedoch vier weitere Prozesse erstellen (insgesamt acht), führt jeder Kern zwei Prozesse gleichzeitig aus, bis sie abgeschlossen sind.
Threads
Threads sind wie Prozesse: Sie haben ihren eigenen Anweisungszeiger und können eine JavaScript-Aufgabe gleichzeitig ausführen. Im Gegensatz zu Prozessen haben Threads jedoch keinen eigenen Speicher. Stattdessen befinden sie sich im Speicher eines Prozesses. Wenn Sie einen Prozess erstellen, können mehrere Threads mit dem Modul worker_threads
erstellt werden, die JavaScript-Code parallel ausführen. Darüber hinaus können Threads miteinander kommunizieren, indem sie Nachrichten austauschen oder Daten im Speicher des Prozesses teilen. Dies macht sie im Vergleich zu Prozessen leichtgewichtig, da das Erstellen eines Threads kein zusätzliches Speicherplatz vom Betriebssystem erfordert.
Was die Ausführung von Threads betrifft, so verhalten sie sich ähnlich wie Prozesse. Wenn mehrere Threads auf einem Einzelkernsystem ausgeführt werden, wechselt das Betriebssystem in regelmäßigen Abständen zwischen ihnen und gibt jedem Thread die Möglichkeit, direkt auf der einzelnen CPU ausgeführt zu werden. Auf einem Mehrkernsystem plant das Betriebssystem die Threads auf allen Kernen und führt den JavaScript-Code gleichzeitig aus. Wenn Sie mehr Threads erstellen als verfügbare Kerne vorhanden sind, führt jeder Kern mehrere Threads gleichzeitig aus.
Mit diesem Befehl drücken Sie ENTER
und stoppen dann alle derzeit laufenden Node-Prozesse mit dem Befehl kill
:
- sudo kill -9 `pgrep node`
pgrep
gibt die Prozess-IDs aller vier Node-Prozesse an den Befehl kill
zurück. Die Option -9
weist kill
an, ein SIGKILL-Signal zu senden.
Wenn Sie den Befehl ausführen, sehen Sie eine Ausgabe ähnlich der folgenden:
Output[1] Killed node process.js A
[2] Killed node process.js B
[3] Killed node process.js C
[4] Killed node process.js D
Manchmal kann die Ausgabe verzögert erscheinen und erst dann auftreten, wenn Sie später einen anderen Befehl ausführen.
Jetzt, da Sie den Unterschied zwischen einem Prozess und einem Thread kennen, werden Sie im nächsten Abschnitt mit den versteckten Threads von Node.js arbeiten.
Verständnis der versteckten Threads in Node.js
Node.js bietet zusätzliche Threads, weshalb es als mehrfädig betrachtet wird. In diesem Abschnitt werden Sie versteckte Threads in Node.js untersuchen, die dazu beitragen, dass I/O-Operationen nicht blockierend sind.
Wie in der Einleitung erwähnt, ist JavaScript single-threaded und der gesamte JavaScript-Code wird in einem einzigen Thread ausgeführt. Dies umfasst Ihren Programmquellcode und Drittanbieterbibliotheken, die Sie in Ihr Programm einbinden. Wenn ein Programm eine I/O-Operation zum Lesen einer Datei oder eine Netzwerkanforderung durchführt, blockiert dies den Hauptthread.
Jedoch implementiert Node.js die libuv
-Bibliothek, die einem Node.js-Prozess vier zusätzliche Threads bereitstellt. Mit diesen Threads werden die E/A-Operationen separat behandelt, und wenn sie abgeschlossen sind, fügt die Ereignisschleife den mit der E/A-Aufgabe verknüpften Rückruf in eine Mikroaufgabenwarteschlange ein. Wenn der Aufrufstapel im Hauptthread frei ist, wird der Rückruf auf den Aufrufstapel geschoben und dann ausgeführt. Um dies klarzustellen, wird der mit der gegebenen E/A-Aufgabe verbundene Rückruf nicht parallel ausgeführt; jedoch erfolgt die Aufgabe selbst, eine Datei zu lesen oder eine Netzwerkanforderung mit Hilfe der Threads, parallel. Sobald die E/A-Aufgabe abgeschlossen ist, wird der Rückruf im Hauptthread ausgeführt.
Zusätzlich zu diesen vier Threads stellt der V8-Motor auch zwei Threads für die Bearbeitung von Dingen wie automatischer Speicherbereinigung bereit. Dies erhöht die Gesamtzahl der Threads in einem Prozess auf sieben: einen Hauptthread, vier Node.js-Threads und zwei V8-Threads.
Um zu bestätigen, dass jeder Node.js-Prozess sieben Threads hat, führen Sie die Datei process.js
erneut aus und setzen Sie sie in den Hintergrund:
- node process.js A &
Das Terminal protokolliert die Prozess-ID sowie die Ausgabe des Programms:
Output[1] 9933
A: 2000
A: 4000
Notieren Sie sich die Prozess-ID irgendwo und drücken Sie ENTER
, damit Sie die Eingabeaufforderung erneut verwenden können.
Um die Threads zu sehen, führen Sie den Befehl top
aus und geben Sie ihm die in der Ausgabe angezeigte Prozess-ID:
- top -H -p 9933
-H
weist top
an, Threads in einem Prozess anzuzeigen. Die -p
-Flagge weist top
an, nur die Aktivität in der angegebenen Prozess-ID zu überwachen.
Wenn Sie den Befehl ausführen, wird Ihre Ausgabe ähnlich aussehen wie folgt:
Outputtop - 09:21:11 up 15:00, 1 user, load average: 0.99, 0.60, 0.26
Threads: 7 total, 1 running, 6 sleeping, 0 stopped, 0 zombie
%Cpu(s): 24.8 us, 0.3 sy, 0.0 ni, 75.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
MiB Mem : 7951.2 total, 6756.1 free, 248.4 used, 946.7 buff/cache
MiB Swap: 0.0 total, 0.0 free, 0.0 used. 7457.4 avail Mem
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
9933 node-us+ 20 0 597936 51864 33956 R 99.9 0.6 4:19.64 node
9934 node-us+ 20 0 597936 51864 33956 S 0.0 0.6 0:00.00 node
9935 node-us+ 20 0 597936 51864 33956 S 0.0 0.6 0:00.84 node
9936 node-us+ 20 0 597936 51864 33956 S 0.0 0.6 0:00.83 node
9937 node-us+ 20 0 597936 51864 33956 S 0.0 0.6 0:00.93 node
9938 node-us+ 20 0 597936 51864 33956 S 0.0 0.6 0:00.83 node
9939 node-us+ 20 0 597936 51864 33956 S 0.0 0.6 0:00.00 node
Wie Sie aus der Ausgabe sehen können, hat der Node.js-Prozess insgesamt sieben Threads: einen Hauptthread zur Ausführung von JavaScript, vier Node.js-Threads und zwei V8-Threads.
Wie zuvor diskutiert, werden die vier Node.js-Threads für I/O-Operationen verwendet, um sie nicht blockierend zu machen. Sie eignen sich gut für diese Aufgabe, und das Erstellen eigener Threads für I/O-Operationen kann sogar die Leistung Ihrer Anwendung verschlechtern. Das Gleiche kann man nicht über CPU-gebundene Aufgaben sagen. Eine CPU-gebundene Aufgabe nutzt keine zusätzlichen Threads im Prozess und blockiert den Hauptthread.
Drücken Sie jetzt „q“, um „top“ zu beenden, und beenden Sie den Node-Prozess mit folgendem Befehl:
- kill -9 9933
Jetzt, da Sie über die Threads in einem Node.js-Prozess Bescheid wissen, werden Sie im nächsten Abschnitt eine CPU-gebundene Aufgabe schreiben und beobachten, wie sie den Hauptthread beeinflusst.
Erstellen einer CPU-gebundenen Aufgabe ohne Worker-Threads
In diesem Abschnitt werden Sie eine Express-App erstellen, die eine nicht blockierende Route und eine blockierende Route enthält, die eine CPU-gebundene Aufgabe ausführt.
Öffnen Sie zunächst „index.js“ in Ihrem bevorzugten Editor:
- nano index.js
Fügen Sie in Ihrer „index.js“-Datei den folgenden Code hinzu, um einen einfachen Server zu erstellen:
const express = require("express");
const app = express();
const port = process.env.PORT || 3000;
app.get("/non-blocking/", (req, res) => {
res.status(200).send("This page is non-blocking");
});
app.listen(port, () => {
console.log(`App listening on port ${port}`);
});
Im folgenden Code-Block erstellen Sie einen HTTP-Server mit Express. In der ersten Zeile importieren Sie das express
-Modul. Als nächstes setzen Sie die Variable app
, um eine Instanz von Express zu halten. Danach definieren Sie die Variable port
, die die Portnummer enthält, auf der der Server hören soll.
Anschließend verwenden Sie app.get('/non-blocking')
, um die Route zu definieren, auf der GET
-Anforderungen gesendet werden sollen. Schließlich rufen Sie die Methode app.listen()
auf, um den Server anzuweisen, auf Port 3000
zu hören.
Definieren Sie als nächstes eine weitere Route, /blocking/
, die eine CPU-intensive Aufgabe enthalten wird:
...
app.get("/blocking", async (req, res) => {
let counter = 0;
for (let i = 0; i < 20_000_000_000; i++) {
counter++;
}
res.status(200).send(`result is ${counter}`);
});
app.listen(port, () => {
console.log(`App listening on port ${port}`);
});
Sie definieren die /blocking
-Route mit app.get("/blocking")
, die als zweites Argument einen asynchronen Rückruf mit dem Präfix async
akzeptiert, der eine CPU-intensive Aufgabe ausführt. Innerhalb des Rückrufs erstellen Sie eine for
-Schleife, die 20 Milliarden Mal durchläuft, und während jeder Iteration inkrementiert sie die Variable counter
um 1
. Diese Aufgabe wird auf der CPU ausgeführt und dauert einige Sekunden, um abzuschließen.
Zu diesem Zeitpunkt wird Ihre index.js
-Datei nun so aussehen:
const express = require("express");
const app = express();
const port = process.env.PORT || 3000;
app.get("/non-blocking/", (req, res) => {
res.status(200).send("This page is non-blocking");
});
app.get("/blocking", async (req, res) => {
let counter = 0;
for (let i = 0; i < 20_000_000_000; i++) {
counter++;
}
res.status(200).send(`result is ${counter}`);
});
app.listen(port, () => {
console.log(`App listening on port ${port}`);
});
Speichern und beenden Sie Ihre Datei und starten Sie dann den Server mit folgendem Befehl:
- node index.js
Wenn Sie den Befehl ausführen, sehen Sie eine Ausgabe ähnlich wie folgt:
OutputApp listening on port 3000
Dies zeigt an, dass der Server läuft und bereit ist, Anfragen zu bedienen.
Besuchen Sie jetzt http://localhost:3000/non-blocking
in Ihrem bevorzugten Browser. Sie werden eine sofortige Antwort mit der Nachricht Diese Seite ist nicht blockierend
sehen.
Hinweis: Wenn Sie das Tutorial auf einem Remote-Server verfolgen, können Sie Portweiterleitung verwenden, um die App im Browser zu testen.
Während der Express-Server noch läuft, öffnen Sie ein weiteres Terminal auf Ihrem lokalen Computer und geben Sie den folgenden Befehl ein:
- ssh -L 3000:localhost:3000 your-non-root-user@yourserver-ip
Nachdem Sie sich mit dem Server verbunden haben, navigieren Sie zu http://localhost:3000/non-blocking
im Webbrowser Ihres lokalen Computers. Behalten Sie das zweite Terminal während des gesamten Rests dieses Tutorials geöffnet.
Öffnen Sie als nächstes einen neuen Tab und besuchen Sie http://localhost:3000/blocking
. Während die Seite lädt, öffnen Sie schnell zwei weitere Tabs und besuchen Sie http://localhost:3000/non-blocking
erneut. Sie werden feststellen, dass Sie keine sofortige Antwort erhalten und die Seiten weiterhin versuchen zu laden. Erst nachdem die /blocking
-Route geladen ist und eine Antwort result is 20000000000
zurückgibt, werden auch die anderen Routen eine Antwort zurückgeben.
Der Grund, warum alle /non-blocking
-Routen nicht funktionieren, während die /blocking
-Route geladen wird, liegt am CPU-gebundenen for
-Loop, der den Hauptthread blockiert. Wenn der Hauptthread blockiert ist, kann Node.js keine Anfragen bedienen, bis die CPU-gebundene Aufgabe abgeschlossen ist. Wenn Ihre Anwendung also Tausende gleichzeitiger GET
-Anfragen an die /non-blocking
-Route hat, genügt bereits ein Besuch der /blocking
-Route, um alle Routen der Anwendung nicht mehr reagieren zu lassen.
Wie Sie sehen können, kann das Blockieren des Hauptthreads die Benutzererfahrung mit Ihrer App beeinträchtigen. Um dieses Problem zu lösen, müssen Sie die CPU-intensive Aufgabe in einen anderen Thread auslagern, damit der Hauptthread weiterhin andere HTTP-Anfragen verarbeiten kann.
Mit diesem Ziel stoppen Sie den Server, indem Sie CTRL+C
drücken. Sie starten den Server im nächsten Abschnitt erneut, nachdem Sie weitere Änderungen an der Datei index.js
vorgenommen haben. Der Grund, warum der Server gestoppt wird, ist, dass Node.js nicht automatisch aktualisiert wird, wenn neue Änderungen an der Datei vorgenommen werden.
Jetzt, da Sie die negative Auswirkung einer CPU-intensiven Aufgabe auf Ihre Anwendung verstehen, werden Sie versuchen, das Blockieren des Hauptthreads zu vermeiden, indem Sie Promises verwenden.
Auslagern einer CPU-intensiven Aufgabe unter Verwendung von Promises
Oft greifen Entwickler, wenn sie von der blockierenden Wirkung von CPU-intensiven Aufgaben erfahren, auf Promises zurück, um den Code nicht blockierend zu machen. Dieser Instinkt kommt aus dem Wissen um die Verwendung von nicht blockierenden, auf Promises basierenden I/O-Methoden, wie z.B. readFile()
und writeFile()
. Aber wie Sie gelernt haben, verwenden die I/O-Operationen verborgene Threads von Node.js, was bei CPU-intensiven Aufgaben nicht der Fall ist. Trotzdem werden Sie in diesem Abschnitt die CPU-intensive Aufgabe in ein Promise einwickeln, um zu versuchen, sie nicht blockierend zu machen. Es wird nicht funktionieren, aber es wird Ihnen helfen, den Wert der Verwendung von Worker-Threads zu erkennen, was Sie im nächsten Abschnitt tun werden.
Öffnen Sie die Datei index.js
erneut in Ihrem Editor:
- nano index.js
In Ihrer Datei `index.js` entfernen Sie den hervorgehobenen Code, der die CPU-intensive Aufgabe enthält.
...
app.get("/blocking", async (req, res) => {
let counter = 0;
for (let i = 0; i < 20_000_000_000; i++) {
counter++;
}
res.status(200).send(`result is ${counter}`);
});
...
Fügen Sie anschließend den folgenden hervorgehobenen Code hinzu, der eine Funktion enthält, die ein Versprechen zurückgibt:
...
function calculateCount() {
return new Promise((resolve, reject) => {
let counter = 0;
for (let i = 0; i < 20_000_000_000; i++) {
counter++;
}
resolve(counter);
});
}
app.get("/blocking", async (req, res) => {
res.status(200).send(`result is ${counter}`);
}
Die Funktion `calculateCount()` enthält nun die Berechnungen, die Sie im Handler `blocking` hatten. Die Funktion gibt ein Versprechen zurück, das mit der Syntax `new Promise` initialisiert wird. Das Versprechen nimmt einen Rückruf mit den Parametern `resolve` und `reject` an, die Erfolg oder Misserfolg behandeln. Wenn die `for`-Schleife beendet ist, löst das Versprechen mit dem Wert in der Variable `counter` aus.
Rufen Sie anschließend die Funktion `calculateCount()` im Handler `blocking` in der Datei `index.js` auf:
app.get("/blocking", async (req, res) => {
const counter = await calculateCount();
res.status(200).send(`result is ${counter}`);
});
Hier rufen Sie die Funktion `calculateCount()` mit dem Schlüsselwort `await` auf, um auf die Auflösung des Versprechens zu warten. Sobald das Versprechen erfüllt ist, wird die Variable `counter` auf den aufgelösten Wert gesetzt.
Ihr vollständiger Code wird nun wie folgt aussehen:
const express = require("express");
const app = express();
const port = process.env.PORT || 3000;
app.get("/non-blocking/", (req, res) => {
res.status(200).send("This page is non-blocking");
});
function calculateCount() {
return new Promise((resolve, reject) => {
let counter = 0;
for (let i = 0; i < 20_000_000_000; i++) {
counter++;
}
resolve(counter);
});
}
app.get("/blocking", async (req, res) => {
const counter = await calculateCount();
res.status(200).send(`result is ${counter}`);
});
app.listen(port, () => {
console.log(`App listening on port ${port}`);
});
Speichern Sie Ihre Datei und beenden Sie sie, dann starten Sie den Server erneut:
- node index.js
Öffnen Sie Ihren Webbrowser und besuchen Sie `http://localhost:3000/blocking`. Wenn es geladen wird, laden Sie schnell die Registerkarten `http://localhost:3000/non-blocking` neu. Wie Sie feststellen werden, sind die Routen `non-blocking` immer noch betroffen und sie warten alle darauf, dass die Route `blocking` das Laden abschließt. Weil die Routen immer noch betroffen sind, führen Versprechen keinen JavaScript-Code parallel aus und können nicht verwendet werden, um CPU-gebundene Aufgaben nicht blockierend zu machen.
Mit diesem Befehl stoppen Sie den Anwendungsserver mit CTRL+C
.
Jetzt, da Sie wissen, dass Versprechungen keinen Mechanismus bieten, um CPU-gebundene Aufgaben nicht blockierend zu machen, werden Sie das Node.js worker-threads
-Modul verwenden, um eine CPU-gebundene Aufgabe in einen separaten Thread auszulagern.
Auslagern einer CPU-gebundenen Aufgabe mit dem worker-threads
-Modul
In diesem Abschnitt lagern Sie eine CPU-intensive Aufgabe in einen anderen Thread aus, indem Sie das worker-threads
-Modul verwenden, um das Blockieren des Hauptthreads zu vermeiden. Dazu erstellen Sie eine worker.js
-Datei, die die CPU-intensive Aufgabe enthält. In der Datei index.js
verwenden Sie das worker-threads
-Modul, um den Thread zu initialisieren und die Aufgabe in der Datei worker.js
parallel zum Hauptthread auszuführen. Sobald die Aufgabe abgeschlossen ist, sendet der Worker-Thread eine Nachricht mit dem Ergebnis zurück an den Hauptthread.
Um zu beginnen, überprüfen Sie, ob Sie 2 oder mehr Kerne haben, indem Sie den Befehl nproc
verwenden:
- nproc
Output4
Wenn es zwei oder mehr Kerne anzeigt, können Sie mit diesem Schritt fortfahren.
Als nächstes erstellen und öffnen Sie die Datei worker.js
in Ihrem Texteditor:
- nano worker.js
Fügen Sie in Ihrer Datei worker.js
den folgenden Code hinzu, um das worker-threads
-Modul zu importieren und die CPU-intensive Aufgabe auszuführen:
const { parentPort } = require("worker_threads");
let counter = 0;
for (let i = 0; i < 20_000_000_000; i++) {
counter++;
}
Die erste Zeile lädt das Modul worker_threads
und extrahiert die Klasse parentPort
. Die Klasse stellt Methoden bereit, die Sie verwenden können, um Nachrichten an den Hauptthread zu senden. Als nächstes haben Sie die rechenintensive Aufgabe, die sich derzeit in der Funktion calculateCount()
in der Datei index.js
befindet. Später in diesem Schritt werden Sie diese Funktion aus der Datei index.js
löschen.
Im Anschluss daran fügen Sie den unten markierten Code hinzu:
const { parentPort } = require("worker_threads");
let counter = 0;
for (let i = 0; i < 20_000_000_000; i++) {
counter++;
}
parentPort.postMessage(counter);
Hier rufen Sie die Methode postMessage()
der Klasse parentPort
auf, die eine Nachricht an den Hauptthread sendet und das Ergebnis der rechenintensiven Aufgabe enthält, die in der Variable counter
gespeichert ist.
Speichern Sie Ihre Datei und verlassen Sie sie. Öffnen Sie index.js
in Ihrem Texteditor:
- nano index.js
Da Sie bereits die rechenintensive Aufgabe in worker.js
haben, entfernen Sie den markierten Code aus index.js
:
const express = require("express");
const app = express();
const port = process.env.PORT || 3000;
app.get("/non-blocking/", (req, res) => {
res.status(200).send("This page is non-blocking");
});
function calculateCount() {
return new Promise((resolve, reject) => {
let counter = 0;
for (let i = 0; i < 20_000_000_000; i++) {
counter++;
}
resolve(counter);
});
}
app.get("/blocking", async (req, res) => {
const counter = await calculateCount();
res.status(200).send(`result is ${counter}`);
});
app.listen(port, () => {
console.log(`App listening on port ${port}`);
});
Als Nächstes fügen Sie im Rückruf von app.get("/blocking")
den folgenden Code hinzu, um den Thread zu initialisieren:
const express = require("express");
const { Worker } = require("worker_threads");
...
app.get("/blocking", async (req, res) => {
const worker = new Worker("./worker.js");
worker.on("message", (data) => {
res.status(200).send(`result is ${data}`);
});
worker.on("error", (msg) => {
res.status(404).send(`An error occurred: ${msg}`);
});
});
...
Zuerst importieren Sie das Modul worker_threads
und entpacken die Klasse Worker
. Innerhalb des Rückrufs von app.get("/blocking")
erstellen Sie eine Instanz von Worker
mithilfe des new
-Schlüsselworts, gefolgt von einem Aufruf von Worker
mit dem Dateipfad worker.js
als Argument. Dies erstellt einen neuen Thread, und der Code in der Datei worker.js
beginnt auf einem anderen Kern zu laufen.
Anschließend befestigen Sie ein Ereignis an der worker
-Instanz unter Verwendung der on("message")
-Methode, um das Nachrichtenereignis anzuhören. Wenn die Nachricht mit dem Ergebnis aus der Datei worker.js
empfangen wird, wird sie als Parameter an den Rückruf der Methode übergeben, der eine Antwort an den Benutzer zurückgibt, die das Ergebnis der CPU-lastigen Aufgabe enthält.
Dann befestigen Sie ein weiteres Ereignis an der worker-Instanz unter Verwendung der on("error")
-Methode, um das Fehlerereignis anzuhören. Wenn ein Fehler auftritt, gibt der Rückruf eine 404
-Antwort zurück, die die Fehlermeldung an den Benutzer enthält.
Ihre vollständige Datei sieht nun wie folgt aus:
const express = require("express");
const { Worker } = require("worker_threads");
const app = express();
const port = process.env.PORT || 3000;
app.get("/non-blocking/", (req, res) => {
res.status(200).send("This page is non-blocking");
});
app.get("/blocking", async (req, res) => {
const worker = new Worker("./worker.js");
worker.on("message", (data) => {
res.status(200).send(`result is ${data}`);
});
worker.on("error", (msg) => {
res.status(404).send(`An error occurred: ${msg}`);
});
});
app.listen(port, () => {
console.log(`App listening on port ${port}`);
});
Speichern Sie Ihre Datei und beenden Sie sie, dann starten Sie den Server:
- node index.js
Besuchen Sie erneut den Tab http://localhost:3000/blocking
in Ihrem Webbrowser. Bevor er fertig geladen ist, aktualisieren Sie alle Tabs http://localhost:3000/non-blocking
. Sie sollten nun feststellen, dass sie sofort geladen werden, ohne auf den Abschluss des Ladens der /blocking
-Route zu warten. Dies liegt daran, dass die CPU-lastige Aufgabe in einen anderen Thread ausgelagert wird und der Hauptthread alle eingehenden Anfragen bearbeitet.
Stoppen Sie nun Ihren Server mit CTRL+C
.
Jetzt, da Sie eine CPU-intensive Aufgabe mithilfe eines Worker-Threads nicht blockierend machen können, verwenden Sie vier Worker-Threads, um die Leistung der CPU-intensiven Aufgabe zu verbessern.
Optimierung einer CPU-intensiven Aufgabe mit vier Arbeits-Threads
In diesem Abschnitt teilen Sie die CPU-intensive Aufgabe auf vier Arbeits-Threads auf, damit sie die Aufgabe schneller abschließen und die Ladezeit der /blocking
-Route verkürzen können.
Um mehr Arbeits-Threads an derselben Aufgabe arbeiten zu lassen, müssen Sie die Aufgaben aufteilen. Da die Aufgabe 20 Milliarden Schleifendurchläufe beinhaltet, teilen Sie 20 Milliarden durch die Anzahl der Threads, die Sie verwenden möchten. In diesem Fall sind es 4
. Die Berechnung von 20_000_000_000 / 4
ergibt 5_000_000_000
. Jeder Thread wird also von 0
bis 5_000_000_000
schleifen und den Zähler
um 1
erhöhen. Wenn jeder Thread fertig ist, sendet er eine Nachricht an den Haupt-Thread mit dem Ergebnis. Sobald der Haupt-Thread Nachrichten von allen vier Threads separat empfangen hat, kombinieren Sie die Ergebnisse und senden eine Antwort an den Benutzer.
Sie können denselben Ansatz auch verwenden, wenn Sie eine Aufgabe haben, die über große Arrays iteriert. Wenn Sie beispielsweise 800 Bilder in einem Verzeichnis ändern möchten, können Sie ein Array erstellen, das alle Bildpfade enthält. Teilen Sie dann 800
durch 4
(die Anzahl der Threads) und lassen Sie jeden Thread in einem Bereich arbeiten. Thread eins ändert Bilder vom Array-Index 0
bis 199
, Thread zwei von Index 200
bis 399
und so weiter.
Zuerst überprüfen Sie, dass Sie vier oder mehr Kerne haben:
- nproc
Output4
Erstellen Sie eine Kopie der Datei worker.js
mit dem Befehl cp
:
- cp worker.js four_workers.js
Die aktuellen Dateien index.js
und worker.js
bleiben unberührt, damit Sie sie später erneut ausführen können, um ihre Leistung mit den Änderungen in diesem Abschnitt zu vergleichen.
Öffnen Sie anschließend die Datei four_workers.js
in Ihrem Texteditor:
- nano four_workers.js
Fügen Sie in Ihrer Datei four_workers.js
den hervorgehobenen Code hinzu, um das Objekt workerData
zu importieren:
const { workerData, parentPort } = require("worker_threads");
let counter = 0;
for (let i = 0; i < 20_000_000_000 / workerData.thread_count; i++) {
counter++;
}
parentPort.postMessage(counter);
Zuerst extrahieren Sie das Objekt WorkerData
, das die Daten enthält, die vom Hauptthread übergeben werden, wenn der Thread initialisiert wird (was Sie bald in der Datei index.js
tun werden). Das Objekt hat eine Eigenschaft thread_count
, die die Anzahl der Threads enthält, in diesem Fall 4
. Im nächsten Schritt wird im for
-Schleife der Wert 20_000_000_000
durch 4
geteilt, was zu 5_000_000_000
führt.
Speichern Sie Ihre Datei und schließen Sie sie, kopieren Sie dann die Datei index.js
:
- cp index.js index_four_workers.js
Öffnen Sie die Datei index_four_workers.js
in Ihrem Editor:
- nano index_four_workers.js
Fügen Sie in Ihrer Datei index_four_workers.js
den hervorgehobenen Code hinzu, um eine Thread-Instanz zu erstellen:
...
const app = express();
const port = process.env.PORT || 3000;
const THREAD_COUNT = 4;
...
function createWorker() {
return new Promise(function (resolve, reject) {
const worker = new Worker("./four_workers.js", {
workerData: { thread_count: THREAD_COUNT },
});
});
}
app.get("/blocking", async (req, res) => {
...
})
...
Zuerst definieren Sie die Konstante THREAD_COUNT
, die die Anzahl der Threads enthält, die Sie erstellen möchten. Wenn Sie später mehr Kerne auf Ihrem Server haben, wird das Skalieren das Ändern des Werts von THREAD_COUNT
auf die gewünschte Anzahl von Threads beinhalten.
Als nächstes erstellt die createWorker()
-Funktion ein Versprechen und gibt es zurück. Innerhalb des Promise-Callbacks initialisierst du einen neuen Thread, indem du der Worker
-Klasse den Dateipfad zur Datei four_workers.js
als ersten Argument übergibst. Dann übergibst du ein Objekt als zweites Argument. Als nächstes weist du dem Objekt die workerData
-Eigenschaft zu, die ein weiteres Objekt als Wert hat. Schließlich weist du dem Objekt die thread_count
-Eigenschaft zu, deren Wert die Anzahl der Threads in der Konstanten THREAD_COUNT
ist. Das workerData
-Objekt ist das, auf das du früher in der Datei workers.js
verwiesen hast.
Um sicherzustellen, dass das Versprechen aufgelöst oder einen Fehler wirft, füge die folgenden markierten Zeilen hinzu:
...
function createWorker() {
return new Promise(function (resolve, reject) {
const worker = new Worker("./four_workers.js", {
workerData: { thread_count: THREAD_COUNT },
});
worker.on("message", (data) => {
resolve(data);
});
worker.on("error", (msg) => {
reject(`An error ocurred: ${msg}`);
});
});
}
...
Wenn der Worker-Thread eine Nachricht an den Hauptthread sendet, wird das Versprechen mit den zurückgegebenen Daten aufgelöst. Wenn jedoch ein Fehler auftritt, gibt das Versprechen eine Fehlermeldung zurück.
Jetzt, da du die Funktion definiert hast, die einen neuen Thread initialisiert und die Daten aus dem Thread zurückgibt, wirst du die Funktion in app.get("/blocking")
verwenden, um neue Threads zu erzeugen.
Aber zuerst entferne den folgenden markierten Code, da du diese Funktionalität bereits in der createWorker()
-Funktion definiert hast:
...
app.get("/blocking", async (req, res) => {
const worker = new Worker("./worker.js");
worker.on("message", (data) => {
res.status(200).send(`result is ${data}`);
});
worker.on("error", (msg) => {
res.status(404).send(`An error ocurred: ${msg}`);
});
});
...
Mit dem gelöschten Code füge den folgenden Code hinzu, um vier Arbeits-Threads zu initialisieren:
...
app.get("/blocking", async (req, res) => {
const workerPromises = [];
for (let i = 0; i < THREAD_COUNT; i++) {
workerPromises.push(createWorker());
}
});
...
Zuerst erstellen Sie eine workerPromises
-Variable, die ein leeres Array enthält. Als nächstes iterieren Sie so oft wie der Wert in THREAD_COUNT
, der 4
ist. Während jeder Iteration rufen Sie die createWorker()
-Funktion auf, um einen neuen Thread zu erstellen. Anschließend fügen Sie das Promise-Objekt, das die Funktion zurückgibt, mit der push
-Methode von JavaScript in das workerPromises
-Array ein. Wenn die Schleife endet, wird das workerPromises
-Array vier Promise-Objekte enthalten, die jeweils viermal durch Aufrufen der createWorker()
-Funktion zurückgegeben wurden.
Fügen Sie nun den folgenden hervorgehobenen Code unten hinzu, um auf das Auflösen der Versprechen zu warten und dem Benutzer eine Antwort zurückzugeben:
app.get("/blocking", async (req, res) => {
const workerPromises = [];
for (let i = 0; i < THREAD_COUNT; i++) {
workerPromises.push(createWorker());
}
const thread_results = await Promise.all(workerPromises);
const total =
thread_results[0] +
thread_results[1] +
thread_results[2] +
thread_results[3];
res.status(200).send(`result is ${total}`);
});
Da das workerPromises
-Array Versprechen enthält, die durch Aufrufen von createWorker()
zurückgegeben wurden, setzen Sie der Promise.all()
-Methode das await
-Syntax voran und rufen die all()
-Methode mit workerPromises
als Argument auf. Die Promise.all()
-Methode wartet darauf, dass alle Versprechen im Array aufgelöst werden. Wenn dies geschieht, enthält die Variable thread_results
die Werte, die die Versprechen aufgelöst haben. Da die Berechnungen auf vier Arbeiter aufgeteilt wurden, addieren Sie sie alle zusammen, indem Sie jeden Wert aus thread_results
mit der eckigen Klammernotationssyntax erhalten. Nachdem sie hinzugefügt wurden, geben Sie den Gesamtwert auf die Seite zurück.
Ihre vollständige Datei sollte nun so aussehen:
const express = require("express");
const { Worker } = require("worker_threads");
const app = express();
const port = process.env.PORT || 3000;
const THREAD_COUNT = 4;
app.get("/non-blocking/", (req, res) => {
res.status(200).send("This page is non-blocking");
});
function createWorker() {
return new Promise(function (resolve, reject) {
const worker = new Worker("./four_workers.js", {
workerData: { thread_count: THREAD_COUNT },
});
worker.on("message", (data) => {
resolve(data);
});
worker.on("error", (msg) => {
reject(`An error ocurred: ${msg}`);
});
});
}
app.get("/blocking", async (req, res) => {
const workerPromises = [];
for (let i = 0; i < THREAD_COUNT; i++) {
workerPromises.push(createWorker());
}
const thread_results = await Promise.all(workerPromises);
const total =
thread_results[0] +
thread_results[1] +
thread_results[2] +
thread_results[3];
res.status(200).send(`result is ${total}`);
});
app.listen(port, () => {
console.log(`App listening on port ${port}`);
});
Speichern und schließen Sie Ihre Datei. Bevor Sie diese Datei ausführen, führen Sie zuerst index.js
aus, um die Antwortzeit zu messen:
- node index.js
Als Nächstes öffnen Sie ein neues Terminal auf Ihrem lokalen Computer und geben Sie den folgenden \texttt{curl}-Befehl ein, der misst, wie lange es dauert, eine Antwort von der \texttt{/blocking}-Route zu erhalten:
- time curl --get http://localhost:3000/blocking
Der Befehl \texttt{time} misst, wie lange der \texttt{curl}-Befehl läuft. Der Befehl \texttt{curl} sendet eine HTTP-Anfrage an die angegebene URL und die Option \texttt{–get} weist \texttt{curl} an, eine \texttt{GET}-Anfrage zu machen.
Wenn der Befehl ausgeführt wird, wird Ihre Ausgabe ähnlich aussehen wie diese:
Outputreal 0m28.882s
user 0m0.018s
sys 0m0.000s
Die markierte Ausgabe zeigt, dass es etwa 28 Sekunden dauert, um eine Antwort zu erhalten, was auf Ihrem Computer variieren kann.
Beenden Sie als Nächstes den Server mit \texttt{CTRL+C} und führen Sie die Datei \texttt{index_four_workers.js} aus:
- node index_four_workers.js
Besuchen Sie erneut die \texttt{/blocking}-Route in Ihrem zweiten Terminal:
- time curl --get http://localhost:3000/blocking
Sie werden eine Ausgabe sehen, die mit der folgenden konsistent ist:
Outputreal 0m8.491s
user 0m0.011s
sys 0m0.005s
Die Ausgabe zeigt, dass es etwa 8 Sekunden dauert, was bedeutet, dass Sie die Ladezeit um etwa 70% reduziert haben.
Sie haben erfolgreich die CPU-gebundene Aufgabe mit vier Worker-Threads optimiert. Wenn Sie einen Computer mit mehr als vier Kernen haben, aktualisieren Sie die \texttt{THREAD\_COUNT} auf diese Zahl, und Sie werden die Ladezeit noch weiter reduzieren.
Schlussfolgerung
In diesem Artikel haben Sie eine Node-App mit einer CPU-gebundenen Aufgabe erstellt, die den Hauptthread blockiert. Anschließend haben Sie versucht, die Aufgabe unter Verwendung von Versprechungen nicht blockierend zu machen, was nicht erfolgreich war. Danach haben Sie das Modul worker_threads
verwendet, um die CPU-gebundene Aufgabe in einen anderen Thread zu verschieben, um sie nicht blockierend zu machen. Schließlich haben Sie das Modul worker_threads
verwendet, um vier Threads zu erstellen, um die CPU-intensive Aufgabe zu beschleunigen.
Als nächsten Schritt sehen Sie sich die Dokumentation zu Node.js Worker Threads an, um mehr über die Optionen zu erfahren. Darüber hinaus können Sie sich die piscina
-Bibliothek ansehen, mit der Sie einen Worker-Pool für Ihre CPU-intensive Aufgaben erstellen können. Wenn Sie Ihr Node.js-Wissen weiter vertiefen möchten, sehen Sie sich die Tutorial-Serie How To Code in Node.js an.
Source:
https://www.digitalocean.com/community/tutorials/how-to-use-multithreading-in-node-js