Evolution im Web: ECMAScript 2017

Seite 2: Geteilter Speicher

Inhaltsverzeichnis

Bei Shared Memory and Atomics handelt es sich ebenfalls um ein größeres Feature. Sämtliche JavaScript-Anwendungen sind zunächst auf einen Thread beschränkt. Ihren einzigen Ausführungsfaden müssen sie im Browser unter anderem zusätzlich mit den Prozessen zum Rendern und Zeichnen teilen. Das kann dazu führen, dass einzelne Webseiten den Browser verlangsamen:

Die Meldung kann auf eine blockierende JavaScript-Anwendung hindeuten.

In solchen Fällen läuft oftmals eine JavaScript-Anwendung, die entweder besonders rechenintensive Operationen durchführt oder in eine Endlosschleife übergegangen ist und die Website einfriert.

Die im Zuge von HTML5 eingeführten Web Worker eröffnen Entwicklern die Möglichkeit, rechenintensive Operationen in einem separaten Hintergrund-Thread auszuführen. Jeder Thread erhält dabei eine separate globale Ausführungsumgebung, und zur Kommunikation mit dem UI-Thread steht die Schnittstelle postMessage zur Verfügung. Beim Austausch von Daten über diese Schnittstelle stellt das System etwa durch das Kopieren primitiver Werte oder Deep Cloning von Objektstrukturen sicher, dass Sender und Empfänger niemals Zugriff auf einen gemeinsamen Speicherbereich erhalten. An diesem Punkt kommen die durch ECMAScript 2017 eingeführten Shared Array Buffers ins Spiel. Solche Zwischenspeicher lassen sich über postMessage teilen, wodurch Sender und Empfänger auf einen gemeinsamen Speicherbereich zugreifen.

// script.js
const worker = new Worker('worker.js');

// 5 Elemente * 1 Byte = 5 Byte Speicher
const sharedBuffer =
new SharedArrayBuffer(5 * Uint8Array.BYTES_PER_ELEMENT);

worker.postMessage({sharedBuffer});
const sharedArray = new Uint8Array(sharedBuffer);

Das Beispiel zeigt das Initialisieren eines Web Worker, dessen Quelltext aus der Datei "worker.js" geladen werden soll. Die Funktion erzeugt einen SharedArrayBuffer mit fünf Byte geteiltem Speicher. Die Größe ergibt sich aus dem Typ des Zielarrays Uint8Array, das fünf Elemente enthalten soll, für das bei Uint8 jeweils ein Byte erforderlich ist. Anschließend überträgt die Funktion den sharedBuffer über die postMessage-Schnittstelle an den Worker. Dabei ist zu beachten, dass das System den SharedArrayBuffer in einem Objekt überträgt, das es – wie oben beschrieben – klont. Das Verpacken in ein Objekt geschieht im obigen Listing über die in ECMAScript 2015 eingeführte Shorthand-Syntax {sharedBuffer}, die ein neues Objekt mit einer Eigenschaft namens sharedBuffer erzeugt, deren Wert sharedBuffer entspricht. Bei der direkten Übergabe von sharedBuffer an postMessage würde der Sender den Zugriff auf den Zwischenspeicher verlieren. Abschließend erstellt obiges Beispiel eine Instanz eines Uint8Array, die auf dem geteilten SharedArrayBuffer arbeitet.

Das Gegenstück hierzu zeigt der Quelltext des Web Worker:

// worker.js
self.addEventListener('message', event => {
const {sharedBuffer} = event.data;
const sharedArray = new Uint8Array(sharedBuffer);
});

Er registriert sich auf das Ereignis 'message' – dem Ziel des postMessage-Aufrufs – und nimmt die zugehörigen Ereignisargumente entgegen. In event.data ist der Klon des über postMessage geteilten Objekts zu finden. Die nächste Zeile zeigt die mit ECMAScript 2015 eingeführten destrukturierenden Zuweisungen. Dort wird die Eigenschaft sharedBuffer aus dem übertragenen Objekt extrahiert und unter dem gleichnamigen Bezeichner verfügbar genmacht. Auch der Web Worker kann mit dem Shared Array Buffer eine Instanz eines Uint8Array erzeugen, die auf dem geteilten Speicher arbeitet.

Ganz ohne postMessage: Schreiben auf und Lesen von geteiltem Speicher

Der obige Screenshot zeigt, dass Entwickler Änderungen im Hauptthread ("top") vornehmen können, die sich über die Thread-Grenze und ohne erneuten Aufruf von postMessage im WebWorker abrufen lassen. Dabei gilt jedoch zu beachten, dass die im Screenshot gezeigten Arten der Zuweisung und des Zugriffs aufgrund eines fehlenden Synchronisations-Kontextes nicht garantieren, dass alles wie vorgesehen funktioniert. Insbesondere können sich schwer zu ermittelnde Timing-Bugs ergeben.

Die benötigten Methoden zur Synchronisierung führt ECMAScript 2017 durch die Bereitstellung des globalen Objekts Atomics ein. Es stellt Methoden wie load(), store(), exchange(), sub() oder xor() zur Verfügung, deren Ausführung garantiert nicht unterbrochen wird. Weiterhin stellt das Objekt die Methoden wait() und wake() zur Verfügung, die sich nutzen lassen, um als Worker selbst in einen Wartemodus überzugehen oder andere wartende Agenten aufzuwecken. Die Operationen sind den von Linux bekannten Fast User-space Mutexes (Futexes) nachempfunden.

Der fehlende oder falsche Einsatz dieser Synchronisationsmethoden kann zu Timing-Bugs führen. Aufgrund dieses Gefahrenpotenzials und der komplizierten Handhabung ist davon auszugehen, dass vorrangig spezialisierte Anwendungen, Frameworks und Bibliotheken, die besonders performant Daten zwischen Threads austauschen müssen, Shared Memory and Atomics nutzen.

Google Chrome 60 und die kommenden Ausgaben von Microsoft Edge (16), Mozilla Firefox (55) und Apple Safari (11) unterstützen Shared Memory and Atomics vollumfänglich. Auf der Serverplattform Node.js in Version 8 lässt es sich über ein Flag aktivieren.