Ich hab mir mal einen allgemeinen und für alle Zwecke geeigneten WebWorkerPool. Man kann die Anzahl der Worker skalieren, was auch bei den Test sich spürbar auswirkte. Auch eine ähnliche Weise sind nun auch die WebWorker auf MP4toGIF.com implementiert, was auch da alles spürbar beschleunigt hat.
Beim eigentlichen WebWorker muss man nur das Antwort-Format einhalten:
{
result:{...},
index:0
}
Index ist der Index des Workloads und wurde in event.data.index übergeben wären der eigentliche Workload in event.data.workload zu finden
ist.
function WebWorkerPool(){
this.workerScript = "";
this.workerData = []; // {msg:{},callback:function}
this.callback = null; //global callback for workerData
this.finishCallback = null;
this.finishCounter=0;
this.worker = null;
this.workers = [];
this.callbackWrapper=function(controller){
return function(event){
controller.finishCounter--;
if(event.data.index && controller.workerData[event.data.index].callback){
controller.workerData[event.data.index].callback(event.data.result);
}
else if(controller.callback){
controller.callback(event.data.result);
}
if(controller.finishCounter == 0){
controller.finishCallback();
try{
controller.worker.close();
for(var iW=0;iW < controller.workers.length; iW++){
controller.workers[iW].close();
}
}
catch(e){
}
}
};
};
this.startSerial = function(){
this.startParallel(1);
};
this.startParallel = function(max){
if(!max){
max = 4;
}
this.finishCounter = this.workerData.length;
this.worker = new Worker(this.workerScript);
for(var iW=0; iW<max; iW++){
var worker = new Worker(this.workerScript);
worker.addEventListener('message', this.callbackWrapper(this), false);
this.workers.push(worker);
}
var factor = this.workerData.length / max;
for(var i=0;i<this.workerData.length;i++){
var workerIndex = Math.floor(i/factor);
this.workers[workerIndex].postMessage(
{
workload: this.workerData.msg,
index:i
});
}
};
}
man kann theoretisch jedem Workload eine eigene Callback-Function mit geben. Wobei eine allgemeine für alle wohl meistens reichen wird.
Wenn alle Workloads abgearbeitet sind wird die finale Callback-Function aufgerufen, um die Ergebnisse weiter zu verarbeiten.
Eine der Stärken bei Java sind die Threads und die ExecutorServices, die die Workloads auf einen festen Pool von Threads verteilen und dann die Ergebnisse Sammeln (Future<...>). Man kann natürlich auch alles in einer großen Schleife erledigen. Bei JavaScript hat man aber das Problem, dass nicht stoppende Scripte sehr schnell gestoppt werden. Das mit Timeouts zu lösen scheitert ganz schnell wenn einzelne Workloads zu lange dauern. Aus diesem Grund wurden die WebWorker entwickelt, die es erlauben nebenläufige Vorgänge in JavaScript zu realisieren und somit auch diese Probleme mit Timeouts von Scripts zu umgehen.
WebWorker und das Hauptscript kommunizieren dabei über Nachrichten, die hin und her geschickt werden. WebWorker können dabei entweder die ganze Zeit existieren oder man kann diese auch direkt nach dem erledigen der Aufgabe wieder beenden. In den meisten Fällen ist
dieses Verhalten wohl das Beste.
Man kann z.B. auch in einem Spiel AI-Gegner mit WebWorkern realisieren. Dann muss der WebWorker natürlich nach dem Starten so lange existieren bis er von außen die Nachricht erhält sich zu beenden.
Ein echt guter Einstieg in die Welt der WebWorker ist diese Seite hier http://www.webworkercontest.net
Man schreibt einen WebWorker, der gegen einen zweiten antritt und versucht möglichst viel Fläche des Spielfeldes mit seiner Farbe zu markieren. Die Kommunikation ist sehr einfach. Der WebWorker gibt über postMessage() in einen JavaScript-Object eine Direction an das Hauptscript zurück und bekommt im nächsten Schritt das Ergebnis ob der letzte Schritt funktioniert hat. Anhang dieses Ergebnisses muss der WebWorker seinen nächsten Schritt planen. Der eigentliche WebWorker ist in der onmessage-Function gekapselt.
Also schickt das Hauptscript eine Message, onmessage des Workers reagiert und darin wird mit postMessage ein Ergebnis zurück geschickt.
Das ist die Struktur nach der WebWorker funktionieren.
Die Kommunikation hat natürlich Grenzen. Ein Element aus dem DOM an einen WebWorker zu über geben und dann dort zu ändern funktioniert (wie man wohl schon erwartet hat) nicht. Wenn man mit einem Canvas etwas machen will muss man die getImageData() Methode bemühen.
Wenn man mit einem WebWorker arbeitet hat man oft den Wunsch auch hier mit Scripten aus externen JS-Files zu arbeiten. Die klassische Variante mit den <script>-Tags funktioniert hier natürlich nicht. Dafür gibt es die importScripts-Function. Der Pfad ist relativ zur Datei des
WebWorkers anzugeben.
Beispiel:
importScripts('../lib/libwebp-0.1.3.demin.js');
Anstelle von onmessage direkt im Script kann man die Haupt-Function auch natürlich über addEventListener setzen, was sehr viel sauberer
aussieht.
Beispiel:
self.addEventListener('message', function(event) {...},false);
Es sollte sowie so beim WebWorker immer "self" verwendet werden.
Die an den WebWorker übermittelten Daten erhält man ganz klassisch über das Event.
var data=event.data;
Was in den Daten drin steht hat man ja selbst bestimmt.
Am Ende der Haupt-Function wird dann das Ergebnis zurück geschickt und wenn gewünscht der WebWorker von sich heraus auch beendet. Der WebWorker kann auch von außen beendet werden, aber es ist wohl sicherer, wenn er sich selbst schließt.
Beispiel:
self.postMessage({index:i,image:result});
self.close();
So. Nachdem wir nun wissen wie der WebWorder intern funktioniert, bleibt am Ende eigentlich nur noch die Frage, wie man nun so einen WebWorker aus dem Haupt-Script heraus startet und wie man die Ergebnisse entgegen nehmen kann. Das ist aber an sich nicht wirklich kompliziert und da alles ja auch Event-Listener und Events setzt, ist nicht schwer zu erraten wie zurück geschickt Messages verarbeitet werden können.
function func(controller){
return function(event){
controller.doSomeThing(event.data);
};
};
var worker = new Worker('./controllers/webpWorker.js');
worker.addEventListener('message', func(this), false);
worker.postMessage(post);
Das Closure der Funktion ist noch das komplexeste hier dran. Der Code hier wird in einer Methode des Controllers ausgeführt und um die Verarbeitung des Ergebnisses in einer anderen Methode des Controllers durch zu führen muss eben der Controller der Function mit einem Binding an das WebWorker-Object im Hauptscript bekannt sein. Closures sind sehr wichtig und ohne diese JavaScript zu schreiben ist extrem umständlich und depremierent. Also wenn das Konzept noch nicht kennt, sich das als erstes erst einmal ansehen!
Die in "post" übergeben Daten findet man im Event im WebWorker unter event.data wieder.
Das hier war jetzt doch relativ kurz gehalten, aber zeigt hoffentlich die Hauptstrukturen sehr gut und reicht für erste Experimente.