JavaScript: Web Workers und Conway's Game of Life in 3D
Optimierung
Der Worker-Code lässt sich als neuer Nachrichtentyp in die bestehende Datei übernehmen. Wenn aus Sicht des Projekts keine Sachverhalte vermischt werden, dient das der Optimierung beim Laden der Seite. Der Browser cached die Datei, sodass der Overhead eines HTTP-Requests für weitere Worker entfällt. Da nur eine Methode der Universum-Klasse aufzurufen ist, sind die meisten Code-Zeilen für den Transfer der Daten notwendig.
//die Nachrichtenverarbeitung für generierePositionsArrayWorker
} else if(event.data.action == 'array') {
var quelle = new Universum(optionen.groesse, event.data.quelle);
var positionsArray =
quelle.generierePositionsArray(event.data.positionsArrayBuffer);
var transfer = quelle.buffers();
transfer.push(positionsArray.buffer);
self.postMessage({
positionsArrayBuffer: positionsArray.buffer,
positionsArrayLaenge: positionsArray.length,
quelle: quelle.buffers()
}, transfer);
}
Etwas komplexer sind die notwendigen Änderungen für die optimierte Berechnung der nächsten Generation. Da im Worker nur noch einzelne Ebenen verarbeitet werden, ist es sinnvoll, die Spiellogik in eine GameOfLife3dEbene-Klasse zu verschieben.
// Spiellogik für eine einzelne Ebene
var GameOfLife3dEbene = function(optionen) {
this.berechneNaechsteGeneration = function(
quelle,
ziel,
z,
callback) {
var todOderLebendig = function(zelle, nachbarn) {
...
};
for(var x=0; x<optionen.groesse; x++) {
for(var y=0; y<optionen.groesse; y++) {
ziel.set(
x,
y,
z,
todOderLebendig(quelle.get(x, y, z),
quelle.zaehleNachbarn(x, y, z)));
}
}
callback();
};
};
Für jede Zielebene sind drei Quellebenen nötig. In der Methode berechneNaechsteGeneration arbeitet man deshalb in Runden, wobei in jeder so viele Dreier-Pakete wie möglich abgearbeitet werden. Die nächste Runde startet erst, wenn alle Worker fertig sind. In der Methode verwaltet die Funktion berechneNaechsteRunde das Abarbeiten der Pakete. behandleNachricht transferiert die Daten in die ursprünglichen Universen zurück. behandleLeereQueue startet die nächste Runde oder verarbeitet die übrigen Ebenen und ruft nach dem Durchlauf von drei Runden die Callback-Funktion auf. Der Einfachheit halber werden die maximal zwei Ebenen synchron ohne Worker verarbeitet.
Synchron in die nächste Runde
Um alle Worker einer Runde zu synchronisieren, kommt statt eines normalen Web Workers ein ParallelWorker aus der Bibliothek WorkerUtils zum Einsatz (Code siehe Exkurs "Spiellogik mit ParallelWorkers implementiert"). Er erstellt mehrere Web Worker und verteilt Nachrichten an nicht aktive Instanzen. Ein Worker gilt so lange als aktiv, bis er auf eine vom Haupt-Event-Handler gesendete Nachricht antwortet.
Grundsätzlich könnte der Worker mehrere Nachrichten senden, unabhängig davon ob er selbst eine erhalten hat. Rechenintensive Funktionen lassen sich auf dem Weg jedoch einfach in eine asynchrone Variante konvertieren. Der ParallelWorker gibt die Aufrufe und Events transparent an die Web Worker weiter.
Neben der postMessage-Methode gibt es noch eine postBroadcastMessage-Methode, die eine Nachricht an alle Web Worker verteilt. Das Programm erzeugt ein emptyqueue-Event, wenn alle Nachrichten verarbeitet wurden. Es lässt sich für die Synchronisierung der Runden nutzen.
Der Code des zu behandelnden Nachrichtentyps ebene im Worker muss sich wiederum größtenteils um den Datentransfer kümmern.
//Das Nachrichtenhandling für den Typ ebene kümmert sich
//hauptsächlich um den Datentransfer.
} else if(event.data.action == 'ebene') {
var quelle = new Universum(
optionen.groesse,
event.data.quelle,
event.data.z);
var ziel = new Universum(
optionen.groesse,
[event.data.ziel],
event.data.z);
gameOfLife3dEbene.berechneNaechsteGeneration(
quelle,
ziel,
event.data.z,
function() {
self.postMessage({
quelle: [
quelle.buffer(event.data.z-1),
quelle.buffer(event.data.z),
quelle.buffer(event.data.z+1)],
ziel: ziel.buffer(event.data.z),
z: event.data.z
}, [
quelle.buffer(event.data.z-1),
quelle.buffer(event.data.z),
quelle.buffer(event.data.z+1),
ziel.buffer(event.data.z)
]);
});
}
Der Code im Controller gleicht nun annähernd wieder der ursprünglichen Version ohne Worker. Nur der synchrone Aufruf zur Berechnung der nächsten Generation musste durch einen asynchronen Aufruf getauscht werden. Das Generieren des Positionsarrays war bereits asynchron implementiert und ließ sich deshalb beibehalten.