JavaScript: Web Workers und Conway's Game of Life in 3D

Parallelisierung

Inhaltsverzeichnis

Als letztes muss man noch die Funktion naechsteGeneration anpassen. Der Worker, in dem die ersten drei Schritte abgearbeitet werden, startet per Nachricht. Mit ihr lässt sich die Variable aktivesUniversum übertragen und der positionsArrayBuffer transferieren, den der ParticleRenderer im Moment nicht verwendet. Nach dem Erledigen der Schritte sendet der Worker eine Antwort. Ein Event Listener für das message-Event verarbeitet die Antwort. Als erstes wird der ArrayBuffer wieder in den Array positionsArrayBuffers geschrieben, damit man in der übernächsten Runde darauf zugreifen kann. Im Anschluss wechselt man das Universum mit einer XOR-Operation auf die Variable aktivesUniversum.

Da als Workaround nur der ArrayBuffer und nicht das TypedArray übertragen wurde, ist nun noch Float32Array zu erstellen. Als vierten und letzten Schritt plant man mit setTimeout die nächste Spielrunde ein.

//Startet den Worker und behandelt die Antwort
var naechsteGeneration = function() {
worker.postMessage({
aktivesUniversum:aktivesUniversum,
positionsArrayBuffer:positionsArrayBuffers[aktivesUniversum^1]
}, [
positionsArrayBuffers[aktivesUniversum^1]
]);
};

worker.addEventListener('message', function(event) {
positionsArrayBuffers[aktivesUniversum^1] =
event.data.positionsArrayBuffer;

aktivesUniversum ^= 1;

positionsArray = new Float32Array(
event.data.positionsArrayBuffer,
0,
event.data.positionsArrayLaenge);

setTimeout(naechsteGeneration, 1);
});

Weiteres Optimierungspotenzial besteht darin, die Berechnungen auf mehrere Worker zu verteilen und Mehrkernsysteme so besser auszulasten. Dabei ist zu beachten, dass Nested Workers, also verschachtelte Worker, im HTML5-Standard vorgesehen sind, der Chrome-Browser sie jedoch noch nicht unterstützt. Lassen sich nur Teile des Codes für mehrere parallel laufende Worker optimieren, ist der restliche Code in einem separaten Worker zu implementieren. Der Controller muss sich dann um die sequenzielle Abarbeitung kümmern. Desweiteren muss man eventuell Strukturen anpassen, da ein Kopiervorgang zu viel Zeit in Anspruch nehmen würde und sich beim Transfer nur ganze ArrayBuffers übertragen lassen.

Laut einer Profiler-Analyse in Chrome benötigt das Berechnen der nächsten Generation die meiste Zeit. An zweiter Stelle kommt das Generieren von Arrays für den ParticleRenderer. Beide Berechnungen lassen sich auf die Z-Ebene verlagern, sodass von den drei for-Schleifen die äußere entfällt und die im Worker befindlichen nur X- und Y-Achse durchlaufen. Da für das Zählen der Nachbarn ein 3x3x3 Zellen großer Würfel benötigt wird, sind drei Quellebenen zu übergeben. Zum Berechnen der nächsten Generation ist genau eine Zielebene nötig.

Beim normalen Generieren des ParticleRenderer-Arrays würde je Ebene ein Array variabler Größe erstellt. Für das gesamte Universum wären Letztere dann zusammen zu kopieren. Durch die Kopiervorgänge ist nicht sicher, ob eine Verteilung auf mehrere Worker von Vorteil wäre. Deshalb ist nur das Berechnen der nächsten Generation zu optimieren.

Zunächst sind die internen Strukturen des Universums anzupassen. Das zellenArray dient nun der Ablage der Ebenen. Der Zugriff über die Z-Achse erfolgt über den Index des zellenArrays. Die Element selbst sind wie bisher Uint8Arrays. Um den existierenden Code weiter verwenden zu können, sollte im Worker weiterhin die Universum-Klasse verwendet werden, auch wenn er nur einen Teil erhält.

Um ein Universum anhand bestehender Daten zu erstellen, erhält der Konstruktor des Universums den zusätzlichen Parameter zellenArrayBuffers. Damit das Programm zudem transparent, auch auf der Z-Achse, auf das Universum zugreifen kann, wenn nur eine oder drei Ebenen vorhanden sind, wird zusätzlich ein zOffset angegeben. Danach ist die Universum-Klasse noch um die Methoden buffer und buffers zu erweitern. So lässt sich lesend und schreibend auf die Ebenen eines bestehenden Universums zugreifen.

Die Parameter sind vom Typ ArrayBuffer und werden in den Methoden zu TypedArrays konvertiert. Ein Work-Around an anderer Stelle ist somit nicht mehr notwendig. Im Anschluss sind nur noch die set- und get-Methoden an die neuen Strukturen anzupassen (siehe Code im Exkurs: "Angepasstes Universum").

Da keine Nested Workers zum Einsatz kommen sollen, muss man den vorletzten Schritt der Spielrunde, das Generieren des Positions-Arrays, in einem eigenen Worker implementieren. Dazu dienen eine neue Methode generierePositionsArrayWorker, eine asynchrone Version vom generierePositionsArray, die bei Bedarf einen Worker anlegt, der die synchrone Methode verwendet. Neben dem positionsArrayBuffer sind noch die ArrayBuffers für das Universum zu übertragen. Die neu angelegte buffers-Methode vereinfacht das Zwischenspeichern beim Senden und Empfangen der Nachricht.

//Asynchrone Wrapper für den Worker-Aufruf

var worker = null;

this.generierePositionsArrayWorker = function(positionsArrayBuffer,
callback) {

// Worker nur bei Bedarf anlegen ->
// Klasse kann auch im Worker verwendet werden
if(worker == null) {
worker = new Worker('gol3d-worker.js');
worker.postMessage({action:'init',optionen:{groesse:groesse}});
}

var universum = this;

var behandleNachricht = function(event) {
worker.removeEventListener('message', behandleNachricht);

// transferierte ArrayBuffer dem ursprünglichem Universum zuordnen
universum.buffers(event.data.quelle);

var positionsArray = new Float32Array(
event.data.positionsArrayBuffer,
0,
event.data.positionsArrayLaenge);

callback(positionsArray);
};

worker.addEventListener('message', behandleNachricht);

// Array mit den zu transferierenden Daten vorbereiten
var transfer = universum.buffers();
transfer.push(positionsArrayBuffer);

// startet die Verarbeitung im Worker an
worker.postMessage({
action: 'array',
quelle: universum.buffers(),
positionsArrayBuffer: positionsArrayBuffer
}, transfer);
};