Am 18. Januar wurde die Node.js-Plattform Version
11.7.0 angekündigt . Unter den bemerkenswerten Änderungen in dieser Version kann man die Schlussfolgerung aus der Kategorie des experimentellen Moduls worker_threads beachten, die in Node.js
10.5.0 veröffentlicht wurde . Jetzt wird die Flagge --experimental-worker nicht mehr benötigt, um sie zu verwenden. Dieses Modul ist seit seiner Einführung ziemlich stabil geblieben, und daher wurde die
Entscheidung getroffen, die sich in Node.js 11.7.0 widerspiegelt.

Der Autor des Materials, dessen Übersetzung wir veröffentlichen, bietet an, die Funktionen des Worker_Threads-Moduls zu diskutieren. Er möchte insbesondere darüber sprechen, warum dieses Modul benötigt wird und wie Multithreading aus historischen Gründen in JavaScript und Node.js implementiert wird. Hier werden wir darüber sprechen, welche Probleme mit dem Schreiben von JS-Anwendungen mit mehreren Threads verbunden sind, über die vorhandenen Lösungsmöglichkeiten und über die Zukunft der parallelen Datenverarbeitung unter Verwendung der sogenannten "Arbeitsthreads", die manchmal als "Arbeitsthreads" bezeichnet werden. oder einfach "Arbeiter".
Leben in einer Welt mit einem einzigen Faden
JavaScript wurde als Single-Threaded-Programmiersprache konzipiert, die in einem Browser ausgeführt wird. "Single-Threaded" bedeutet, dass im selben Prozess (in modernen Browsern sprechen wir von separaten Browser-Registerkarten) jeweils nur ein Befehlssatz ausgeführt werden kann.
Dies vereinfacht die Anwendungsentwicklung und erleichtert die Arbeit der Programmierer. Ursprünglich war JavaScript eine Sprache, die nur zum Hinzufügen einiger interaktiver Funktionen zu Webseiten geeignet war, beispielsweise zur Formularüberprüfung. Unter den Aufgaben, für die JS entwickelt wurde, gab es nichts besonders Kompliziertes, das Multithreading erforderte.
Ryan Dahl , Schöpfer von Node.js, sah eine interessante Gelegenheit in dieser Sprachbeschränkung. Er wollte eine Serverplattform implementieren, die auf einem asynchronen E / A-Subsystem basiert. Dies bedeutete, dass der Programmierer nicht mit Threads arbeiten musste, was die Entwicklung für eine ähnliche Plattform erheblich vereinfacht. Bei der Entwicklung von Programmen für die parallele Codeausführung können Probleme auftreten, die sehr schwer zu lösen sind. Wenn beispielsweise mehrere Threads versuchen, auf denselben Speicherbereich zuzugreifen, kann dies zu einem sogenannten "Process Race State" führen, der das Programm stört. Solche Fehler sind schwer zu reproduzieren und zu korrigieren.
Ist die Node.js-Plattform Single-Threaded?
Sind Node.js Apps Single-Threaded? Ja, in gewisser Weise ist es so. In Node.js können Sie zwar bestimmte Aktionen parallel ausführen, der Programmierer muss jedoch keine Threads erstellen oder synchronisieren. Die Node.js-Plattform und das Betriebssystem führen parallele Eingabe- / Ausgabeoperationen auf eigene Faust aus. Wenn die Zeit für die Datenverarbeitung mit unserem JavaScript-Code gekommen ist, funktioniert dies im Single-Threaded-Modus.
Mit anderen Worten, alles außer unserem JS-Code funktioniert parallel. In synchronen Blöcken von JavaScript-Code werden Befehle immer einzeln in der Reihenfolge ausgeführt, in der sie im Quellcode dargestellt werden:
let flag = false function doSomething() { flag = true
All dies ist großartig - wenn unser gesamter Code mit asynchroner E / A beschäftigt ist. Das Programm besteht aus kleinen Blöcken synchronen Codes, die schnell mit Daten arbeiten, die beispielsweise an Dateien und Streams gesendet werden. Der Code von Programmfragmenten ist so schnell, dass er die Ausführung des Codes seiner anderen Fragmente nicht blockiert. Das Warten auf die Ergebnisse der asynchronen E / A dauert viel länger als die Codeausführung. Betrachten Sie ein kleines Beispiel:
db.findOne('SELECT ... LIMIT 1', function(err, result) { if (err) return console.error(err) console.log(result) }) console.log('Running query') setTimeout(function() { console.log('Hey there') }, 1000)
Es ist möglich, dass die Abfrage an die hier gezeigte Datenbank ungefähr eine Minute dauert, aber die Nachricht "
Running query
wird sofort nach dem Initiieren dieser Abfrage an die Konsole gesendet. In diesem Fall wird die Meldung "
Hey there
eine Sekunde nach Ausführung der Anforderung angezeigt, unabhängig davon, ob die Ausführung abgeschlossen wurde oder nicht. Unsere Node.js-Anwendung ruft einfach die Funktion auf, die die Anforderung initiiert, während die Ausführung des anderen Codes nicht blockiert wird. Nachdem die Anforderung abgeschlossen ist, wird die Anwendung mithilfe der Rückruffunktion darüber informiert und erhält dann eine Antwort auf diese Anforderung.
CPU-intensive Aufgaben
Was passiert, wenn wir über JavaScript Heavy Computing betreiben müssen? Zum Beispiel - um einen großen Datensatz zu verarbeiten, der im Speicher gespeichert ist? Dies kann dazu führen, dass das Programm ein Fragment von synchronem Code enthält, dessen Ausführung viel Zeit in Anspruch nimmt und die Ausführung von anderem Code blockiert. Stellen Sie sich vor, diese Berechnungen dauern 10 Sekunden. Wenn es sich um einen Webserver handelt, der eine bestimmte Anforderung verarbeitet, bedeutet dies, dass er mindestens 10 Sekunden lang keine anderen Anforderungen verarbeiten kann. Das ist ein großes Problem. Berechnungen, die länger als 100 Millisekunden sind, können dieses Problem bereits verursachen.
JavaScript und die Node.js-Plattform wurden ursprünglich nicht entwickelt, um Aufgaben zu lösen, bei denen Prozessorressourcen intensiv genutzt werden. Wenn JS im Browser ausgeführt wird, bedeutet das Ausführen solcher Aufgaben "Bremsen" auf der Benutzeroberfläche. In Node.js kann dies die Möglichkeit einschränken, die Plattform zur Ausführung neuer asynchroner E / A-Aufgaben aufzufordern und auf Ereignisse zu reagieren, die mit deren Abschluss verbunden sind.
Kehren wir zu unserem vorherigen Beispiel zurück. Stellen Sie sich vor, als Antwort auf eine Anfrage an die Datenbank gingen mehrere tausend verschlüsselte Datensätze ein, die im synchronen JS-Code entschlüsselt werden müssen:
db.findAll('SELECT ...', function(err, results) { if (err) return console.error(err)
Die Ergebnisse befinden sich nach dem Empfang in der Rückruffunktion. Danach kann bis zum Ende ihrer Verarbeitung kein anderer JS-Code ausgeführt werden. Wie bereits erwähnt, ist die Belastung des durch diesen Code erzeugten Systems normalerweise minimal und führt die ihm zugewiesenen Aufgaben schnell aus. In diesem Fall hat das Programm jedoch die Abfrageergebnisse erhalten, die eine beträchtliche Menge haben, und wir müssen sie noch verarbeiten. So etwas kann einige Sekunden dauern. Wenn es sich um einen Server handelt, mit dem viele Benutzer arbeiten, bedeutet dies, dass sie erst nach Abschluss eines ressourcenintensiven Vorgangs weiterarbeiten können.
Warum wird JavaScript niemals Threads haben?
In Anbetracht des oben Gesagten scheint es, dass Sie zur Lösung schwerwiegender Computerprobleme in Node.js ein neues Modul hinzufügen müssen, mit dem Sie Threads erstellen und verwalten können. Wie kann man auf so etwas verzichten? Es ist sehr traurig, dass diejenigen, die eine ausgereifte Serverplattform wie Node.js verwenden, nicht über die Mittel verfügen, um Probleme im Zusammenhang mit der Verarbeitung großer Datenmengen auf wunderbare Weise zu lösen.
All dies ist wahr, aber wenn Sie die Möglichkeit hinzufügen, mit Streams in JavaScript zu arbeiten, führt dies zu einer Änderung der Natur dieser Sprache. In JS können Sie nicht einfach die Möglichkeit hinzufügen, mit Threads zu arbeiten, z. B. in Form neuer Klassen oder Funktionen. Dazu müssen Sie die Sprache selbst ändern. In Sprachen, die Multithreading unterstützen, ist das Konzept der Synchronisation weit verbreitet. In Java sind beispielsweise sogar
einige numerische Typen nicht atomar. Dies bedeutet, dass, wenn Synchronisationsmechanismen nicht verwendet werden, um mit ihnen von verschiedenen Threads aus zu arbeiten, dies beispielsweise dazu führen kann, dass mehrere Bytes einer solchen Variablen auf eins gesetzt werden, nachdem mehrere Threads gleichzeitig versucht haben, den Wert derselben Variablen zu ändern fließen und ein paar andere. Infolgedessen enthält eine solche Variable etwas, das mit dem normalen Betrieb des Programms nicht kompatibel ist.
Primitive Lösung des Problems: Iteration der Ereignisschleife
Node.js führt den nächsten Codeblock in der Ereigniswarteschlange erst aus, wenn der vorherige Block abgeschlossen ist. Dies bedeutet, dass wir unser Problem zur Lösung in Teile
setImmediate(callback)
können, die durch synchrone Codefragmente dargestellt werden, und dann eine Konstruktion des Formulars
setImmediate(callback)
verwenden können, um die Ausführung dieser Fragmente zu planen. Der von der
callback
in diesem Konstrukt angegebene Code wird ausgeführt, nachdem die Aufgaben der aktuellen Iteration (Tick) der Ereignisschleife abgeschlossen sind. Danach wird das gleiche Design verwendet, um den nächsten Stapel von Berechnungen in die Warteschlange zu stellen. Dies ermöglicht es, den Zyklus von Ereignissen nicht zu blockieren und gleichzeitig volumetrische Probleme zu lösen.
Stellen Sie sich vor, wir haben ein großes Array, das verarbeitet werden muss, während die Verarbeitung jedes Elements eines solchen Arrays komplexe Berechnungen erfordert:
const arr = [] for (const item of arr) {
Wie bereits erwähnt, dauert es zu lange, bis das gesamte Array in einem Aufruf verarbeitet wird, und die Ausführung eines anderen Anwendungscodes wird verhindert. Daher werden wir diese große Aufgabe in Teile
setImmediate(callback)
und das
setImmediate(callback)
verwenden:
const crypto = require('crypto') const arr = new Array(200).fill('something') function processChunk() { if (arr.length === 0) {
Jetzt verarbeiten wir auf einmal zehn Elemente des Arrays.
setImmediate()
planen wir mit
setImmediate()
den nächsten Berechnungsstapel. Dies bedeutet, dass, wenn Sie mehr Code im Programm ausführen müssen, dieser zwischen Operationen zum Verarbeiten von Fragmenten des Arrays ausgeführt werden kann. Dafür gibt es hier am Ende des Beispiels Code, der
setInterval()
.
Wie Sie sehen können, sieht ein solcher Code viel komplizierter aus als seine ursprüngliche Version. Und oft kann der Algorithmus viel komplexer sein als der unsere, was bedeutet, dass es bei der Implementierung nicht einfach ist, die Berechnungen in Teile zu
setImmediate()
und zu verstehen, wo Sie
setImmediate()
setzen müssen, um das richtige Gleichgewicht zu erreichen, um das richtige Gleichgewicht zu erreichen. Außerdem stellte sich heraus, dass der Code jetzt asynchron ist. Wenn unser Projekt von Bibliotheken von Drittanbietern abhängt, können wir den Prozess der Lösung einer schwierigen Aufgabe möglicherweise nicht in Teile aufteilen.
Hintergrundprozesse
Vielleicht
setImmediate()
der obige Ansatz mit
setImmediate()
in einfachen Fällen gut, ist aber
setImmediate()
andere als ideal. Außerdem werden hier (aus offensichtlichen Gründen) keine Threads verwendet, und wir beabsichtigen auch nicht, die Sprache dafür zu ändern. Ist es möglich, parallele Datenverarbeitung ohne Threads durchzuführen? Ja, das ist möglich, und dafür brauchen wir einen Mechanismus für die Hintergrunddatenverarbeitung. Es geht darum, eine bestimmte Aufgabe zu starten, Daten an sie zu übergeben, und dass diese Aufgabe, ohne den Hauptcode zu beeinträchtigen, alles verwendet, was sie benötigt, so viel Zeit für die Arbeit benötigt, wie sie benötigt, und dann die Ergebnisse an zurückgibt Hauptcode. Wir brauchen etwas Ähnliches wie das folgende Code-Snippet:
Die Realität ist, dass Sie in Node.js Hintergrundprozesse verwenden können. Der Punkt ist, dass es möglich ist, einen Zweig des Prozesses zu erstellen und das oben beschriebene Arbeitsschema unter Verwendung des Mechanismus des Messaging zwischen dem untergeordneten und dem übergeordneten Prozess zu implementieren. Der Hauptprozess kann mit dem untergeordneten Prozess interagieren, Ereignisse an ihn senden und von ihm empfangen. Shared Memory wird bei diesem Ansatz nicht verwendet. Alle von Prozessen ausgetauschten Daten werden „geklont“. Wenn also von einem Prozess Änderungen an einer Instanz dieser Daten vorgenommen werden, sind diese Änderungen für einen anderen Prozess nicht sichtbar. Dies ähnelt einer HTTP-Anforderung. Wenn ein Client sie an den Server sendet, erhält der Server nur eine Kopie davon. Wenn Prozesse keinen gemeinsamen Speicher verwenden, bedeutet dies, dass es bei gleichzeitigem Betrieb unmöglich ist, einen „Race-Status“ zu erstellen, und dass wir uns nicht mit der Arbeit mit Threads belasten müssen. Es scheint, dass unser Problem gelöst wurde.
In Wirklichkeit ist das nicht so. Ja - vor uns liegt eine der Lösungen für die Aufgabe, intensive Berechnungen durchzuführen, aber es ist wiederum unvollkommen. Das Erstellen einer Verzweigung eines Prozesses ist eine ressourcenintensive Operation. Es braucht Zeit, um es abzuschließen. Tatsächlich geht es darum, eine neue virtuelle Maschine von Grund auf neu zu erstellen und den vom Programm belegten Speicherplatz zu erhöhen, was darauf zurückzuführen ist, dass Prozesse keinen gemeinsam genutzten Speicher verwenden. In Anbetracht des Vorstehenden ist es angebracht zu fragen, ob es nach Abschluss einer Aufgabe möglich ist, die Abzweigung des Prozesses wiederzuverwenden. Sie können diese Frage positiv beantworten, aber hier müssen Sie bedenken, dass geplant ist, den Zweig des Prozesses auf verschiedene ressourcenintensive Aufgaben zu übertragen, die synchron ausgeführt werden. Hier sind zwei Probleme zu sehen:
- Obwohl bei diesem Ansatz der Hauptprozess nicht blockiert wird, kann der untergeordnete Prozess die an ihn übertragenen Aufgaben nur nacheinander ausführen. Wenn wir zwei Aufgaben haben, von denen eine 10 Sekunden und die zweite 1 Sekunde dauert und wir sie in dieser Reihenfolge erledigen, ist es unwahrscheinlich, dass wir warten müssen, bis die erste vor der zweiten erledigt ist. Da wir Prozessgabeln erstellen, möchten wir die Funktionen des Betriebssystems nutzen, um Aufgaben zu planen und die Rechenressourcen aller Kerne unseres Prozessors zu nutzen. Wir brauchen etwas, das der Arbeit an einem Computer ähnelt, für eine Person, die Musik hört und durch Webseiten reist. Dazu können Sie zwei Fork-Prozesse erstellen und mit deren Hilfe die parallele Ausführung von Aufgaben organisieren.
- Wenn eine der Aufgaben mit einem Fehler zum Ende des Prozesses führt, werden alle an einen solchen Prozess gesendeten Aufgaben nicht verarbeitet.
Um diese Probleme zu lösen, benötigen wir mehrere Fork-Prozesse, nicht einen, aber wir müssen ihre Anzahl begrenzen, da jeder von ihnen Systemressourcen benötigt und es Zeit braucht, um jeden von ihnen zu erstellen. Daher benötigen wir nach dem Muster von Systemen, die Datenbankverbindungen unterstützen, so etwas wie einen Pool gebrauchsfertiger Prozesse. Das Prozesspool-Managementsystem verwendet nach Erhalt neuer Aufgaben freie Prozesse, um diese auszuführen, und wenn ein bestimmter Prozess mit der Aufgabe fertig wird, kann es eine neue zuweisen. Es besteht das Gefühl, dass ein solches Arbeitsschema nicht einfach umzusetzen ist und tatsächlich auch ist. Wir werden das
Worker-Farm- Paket verwenden, um dieses Schema zu implementieren:
Worker_threads-Modul
Ist unser Problem gelöst? Ja, wir können sagen, dass es gelöst ist, aber mit diesem Ansatz wird viel mehr Speicher benötigt, als erforderlich wäre, wenn wir eine Multithread-Lösung zur Verfügung hätten. Threads verbrauchen im Vergleich zu Prozessgabeln weitaus weniger Ressourcen. Aus diesem Grund wurde das Modul
worker_threads
in
worker_threads
Arbeitsthreads werden in einem isolierten Kontext ausgeführt. Sie tauschen Informationen mit dem Hauptprozess über Nachrichten aus. Dies erspart uns das Problem der „Race Condition“, dem Multithread-Umgebungen ausgesetzt sind. Gleichzeitig existieren Arbeitsabläufe in demselben Prozess wie das Hauptprogramm, d. H. Bei diesem Ansatz wird im Vergleich zur Verwendung von Prozessgabeln viel weniger Speicher verwendet.
Darüber hinaus können Sie bei der Arbeit mit Arbeitern den gemeinsamen Speicher verwenden. Speziell für diesen Zweck werden Objekte vom Typ
SharedArrayBuffer
. Sie sollten nur in den Fällen verwendet werden, in denen das Programm eine komplexe Verarbeitung großer Datenmengen durchführen muss. Mit ihnen können Sie die Ressourcen speichern, die zum Serialisieren und Deserialisieren von Daten erforderlich sind, wenn Sie den Datenaustausch zwischen Mitarbeitern und dem Hauptprogramm über Nachrichten organisieren.
Arbeiter Arbeiter fließt
Wenn Sie die Node.js-Plattform vor Version 11.7.0 verwenden, müssen Sie zum Starten von
--experimental-worker
Flag
--experimental-worker
, um die Arbeit mit dem Modul worker_threads zu aktivieren.
Darüber hinaus ist zu beachten, dass das Erstellen eines Workers (wie das Erstellen eines Threads in einer beliebigen Sprache), obwohl es viel weniger Ressourcen erfordert als das Erstellen eines Abzweigs des Prozesses, auch eine gewisse Belastung des Systems verursacht. Vielleicht ist in Ihrem Fall sogar diese Last zu hoch. In solchen Fällen wird in der Dokumentation empfohlen, einen Pool von Arbeitnehmern zu erstellen. Wenn Sie dies benötigen, können Sie natürlich eine eigene Implementierung eines solchen Mechanismus erstellen, aber vielleicht sollten Sie in der NPM-Registrierung nach etwas Passendem suchen.
Betrachten Sie ein Beispiel für die Arbeit mit Arbeitsthreads. Wir werden eine Hauptdatei haben,
index.js
, in der wir einen
index.js
erstellen und ihm einige Daten zur Verarbeitung übergeben. Die entsprechende API ist ereignisbasiert, aber ich werde hier ein Versprechen verwenden, das aufgelöst wird, wenn die erste Nachricht vom Worker eintrifft:
Wie Sie sehen, ist die Verwendung des Workflow-Ablaufmechanismus recht einfach. Wenn Sie einen Worker erstellen, müssen Sie den Pfad zur Datei mit dem Worker-Code und den Daten an den
Worker
Designer übergeben. Denken Sie daran, dass diese Daten geklont und nicht im gemeinsamen Speicher gespeichert werden. Nach dem Starten des Arbeiters erwarten wir eine Nachricht von ihm, die das
message
abhört.
Oben haben wir beim Erstellen eines Objekts vom Typ
Worker
dem Konstruktor den Namen der Datei mit dem Worker-Code
service.js
. Hier ist der Code für diese Datei:
const { workerData, parentPort } = require('worker_threads')
Es gibt zwei Dinge, die uns am Worker Code interessieren. Zunächst benötigen wir die von der Hauptanwendung übertragenen Daten. In unserem Fall werden sie durch die Variable
workerData
. Zweitens benötigen wir einen Mechanismus zur Übertragung von Informationen an die Hauptanwendung. Dieser Mechanismus wird durch das
parentPort
Objekt dargestellt, das über die
postMessage()
-Methode verfügt, mit der wir die Ergebnisse der Datenverarbeitung an die Hauptanwendung übergeben. So funktioniert alles.
Hier ist ein sehr einfaches Beispiel, aber mit denselben Mechanismen können Sie viel komplexere Strukturen erstellen. Beispielsweise können Sie aus dem Worker-Stream viele Nachrichten an den Haupt-Stream senden, die Informationen zum Status der Datenverarbeitung enthalten, falls unsere Anwendung einen ähnlichen Mechanismus benötigt. Auch vom Mitarbeiter können die Datenverarbeitungsergebnisse in Teilen zurückgegeben werden. Zum Beispiel kann so etwas nützlich sein, wenn ein Mitarbeiter beschäftigt ist, beispielsweise Tausende von Bildern verarbeitet, und Sie, ohne auf die Verarbeitung aller Bilder zu warten, die Hauptanwendung über den Abschluss der Verarbeitung jedes einzelnen Bilds informieren möchten.
Details zum Modul
worker_threads
finden Sie
hier .
Web-Worker
Sie haben vielleicht von Web-Workern gehört. Sie sind für den Einsatz in einer Client-Umgebung vorgesehen. Diese Technologie existiert seit langem und wird
von modernen Browsern
gut unterstützt . Die API für die Arbeit mit Web-
worker_threads
unterscheidet sich von der, die uns das Node.js-Modul
worker_threads
bietet. Es geht um die Unterschiede in den Umgebungen, in denen sie arbeiten. Diese Technologien können jedoch ähnliche Probleme lösen. Beispielsweise können Web-Worker in Client-Anwendungen verwendet werden, um die Ver- und Entschlüsselung von Daten sowie deren Komprimierung und Dekomprimierung durchzuführen. Mit ihrer Hilfe können Sie Bilder verarbeiten, Computer-Vision-Systeme implementieren (wir sprechen beispielsweise von Gesichtserkennung) und andere ähnliche Probleme in einem Browser lösen.
Zusammenfassung
worker_threads
— Node.js. , , . , , , « ». , ? ,
worker_threads
, Node.js
worker-farm ,
worker_threads
, Node.js .
Liebe Leser! Node.js-?
