JS Multithreading
Stand: 07/2015
Lesedauer: ca. 4 Minuten
Inhalt:
setTimeout
Web Worker
BLOB Inline Worker
Eigener Worker
Einzeiliger Worker
Fazit
Entwicklung eines einfachen Multithreading-Mechanismus für JavaScript der ohne Aufwand an beliebigen Stellen in jedem Projekt genutzt werden kann.
Nebenläufigkeiten in JavaScript werden meist durch die setTimeout
-Funktion realisiert. Hierfür kann eine gewisse Zeitspanne angegeben werden, wann der JavaScript-Code, welcher der Funktion übergeben wurde,
ausgeführt wird. Zwar werden durch diese Methode unterschiedliche Ausführungsstränge erzeugt, allerdings laufen alle diese Ausführungsstränge auf dem gleichen Thread, was einem Time-Multiplexing und keiner echten
Parallelität entspricht.
Für richtiges Multithreading werden die in JavaScript neu eingeführten Web Worker verwendet. Hierbei kann mithilfe der API ein Hintergrund-Skript erzeugt werden, welches als eigenständiger, nicht blockierender Thread läuft. Der Vorteil dieser Methode ist, dass jeder Thread auf einem eigenen CPU-Kern und somit echt parallel ausgeführt werden kann. So können rechenintensive Teile einer Webanwendung in einen nicht blockierenden und parallelen Bereich berechnet werden.
Um echte Parallelität zu erreichen müssen Web Worker eigenständigen, isolierten Code ausführen, welcher bei der Initialisierung eines Web Workers geladen wird.
var worker = new Worker('workerScript.js');
Jegliche Kommunikation zwischen dem Haupt-Thread und dem Worker läuft über ein internes Ereignisbasiertes Messaging-System. Bei Erhalt der ersten Nachricht über dieses System startet der Worker die Ausführung des JavaScript-Codes.
worker.postMessage();
Wie im vorherigen Abschnitt erläutert, läuft die Kommunikation zwischen Workern und Haupt-Thread über das Nachrichtensystem. Hierfür muss sowohl der Haupt-Thread, als auch der Worker auf Nachrichten reagieren und diese verarbeiten. Für diesen Zweck müssen beide einen eigenen Event-Listener registrieren.
worker.addEventListener('message', function(e) { console.log(e.data); // prints the message }, false);
und
self.addEventListener('message', function(e) { self.postMessage(e.data); // sends the message back }, false);
Aufgrund ihres Multithreading-Verhaltens können Web Worker nur auf einen Teil der Funktionen von JavaScript zugreifen:
Worker haben keinen Zugriff auf:
Soll der auszuführende JavaScript-Code nicht aus einer externen Datei geladen, sondern innerhalb des Main-JavaScript-Codes enthalten sein, ist dies durch die Erstellung und Übergabe eines BLOBs möglich.
Mit dem BlobBuilder kann der Worker und der Main-JavaScript-Codes in derselben Datei enthalten sein. Dazu muss lediglich ein BlobBuilder erstellt und diesem der Worker-Code übergeben werden:
var bb = new BlobBuilder(); bb.append("onmessage = function(e) {self.postMessage(e.data);}"); // creates a BLOB-URL var blobURL = window.URL.createObjectURL(bb.getBlob()); var worker = new Worker(blobURL); worker.onmessage = function(e) { console.log(e.data); }; // start the worker worker.postMessage();
Aus den oben gezeigten Beispielen zur Funktionalität der Web Worker wurde anschließend durch eine Reduzierung des Codes einer Worker entwickelt. Das folgende Beispiel erstellt einen neuen Worker, der Parallel jegliche Aufgabe abarbeiten kann, die ihm als JavaScript-Code übergeben wird. Der zu übergebende Code wird als Funktion behandelt. Dies bedeutet, dass beliebige JavaScript-Funktionalitäten angegeben und aufgerufen werden können. Der Scope ist vom Haupt-Thread hierbei komplett abgetrennt. Am Ende des Worker-Codes wird der return Aufruf verwendet, um das Ergebnis des Workers (wie eine normale Funktion) an den Main-Thread mittels Event-System zurückzugeben. Anschließend kann der Haupt-Thread das Ergebnis beliebig weiter verarbeiten.
// creates the content of the worker var aFilePart = ["onmessage = function(e) {postMessage(eval('(function(){'+e.data+'})();'));}"]; // creates the BLOB and sets the type var oMyBlob = new Blob(aFilePart, {type : 'text/html'}); // creates the URL for the BLOB var blobURL = window.URL.createObjectURL(oMyBlob); // creats the new web worker with the BLOB var worker = new Worker(blobURL); // creates the event listener for the main thread // you have the change the content here! worker.onmessage = function(e) { alert(e.data); }; // a test parameter var rounds = 10; // sent the worker the JS-code to execute as a message worker.postMessage(" var ret = 'Hallo'; for(var i=0; i<"+rounds+"; i++){ ret += ' '+i; }; return ret; ");
Das folgenden Beispiel führt die Reduzierung weiter und zeigt das obige Beispiel in komprimierter einzeiliger Form:
Erstellung des BLOBs welcher für beliebig viele Worker verwendet werden kann.
var oMyBlob = new Blob(["onmessage = function(e) {postMessage(eval('(function(){'+e.data+'})();'));}"], {type : 'text/html'});
Erstellung eines Workers und dessen Event-Handler
var worker = new Worker(window.URL.createObjectURL(oMyBlob)); worker.onmessage = function(e) { // Event-Handler alert(e.data); // < - - CHANGE THIS CONTENT HERE! };
Übergabe von beliebigem JavaScript-Code an den Worker mit gleichzeitigem Start des Workers.
worker.postMessage("JAVASCRIPT-CODE-HERE!");
Mit dieser Implementierung ist es möglich mehrere Web Worker zu erstellen und diese echt parallel Laufen zu lassen. Der BLOB muss hierfür nur einmal erstellt und jedem Worker bei dessen Erstellung übergeben werden.
Ferner wird nicht für jede Ausführung ein neuer Worker benötigt. Ist der Worker fertig, so kann er mit einem erneuten Aufruf der postMessage()
-Funktion mit neuem JavaScript-Code befüllt und die Ausführung
gestartet werden. Die Ausführung des Codes wird sofort bei Erhalt der Nachricht gestartet.
Mit der hier gezeigten Implementierung ist es nach der Initialisierung möglich, beliebigen JavaScript-Code an beliebigen Stellen einer Webanwendung mit nur einem Funktionsaufruf in einem echt parallel laufenden Thread abzuarbeiten.