Web Development➝Web Workers
Overview
Some of the new things that are coming with HTML 5 are pretty cool. One of them is Web Workers which are designed to allow the browser to spawn a background task which runs JavaScript in parallel with the main page. This allows you to do some serious number crunching in the background without freezing the interface.
The basics of web workers are really quite simple. If you have some JavaScript in an external file, you can create a new worker that will run this JavaScript on a separate thread to the UI, you simply do
var oWorker = new Worker('worker_src.js');
Now that isn't much use unless you can get some information back from the worker. To do this the worker's source script has to post messages back to the worker object, this can be done simply by doing the following within the worker_src.js file
postMessage(oMessage);
The value oMessage
that we send can be a string, or more usefully, a JSON object literal. The worker object will recieve this
message and can be handled by doing
oWorker.onmessage = function(e)
{
var oMessage = e.data;
/* process message here */
};
Sending messages the other way is just as straightforward, all we do is call the postMessage method on the worker object, again passing some JSON object literal as the message, as follows
oWorker.postMessage(oMessage);
and handle the message in the worker source file by simply including something like
onmessage = function(e)
{
var oMessage = e.data;
/* process message here */
};
That's all very straightforward, but it's not much use if you can't use it across all the browsers, so what we need is some way of emulating the web worker functionality in those browsers that don't yet support it. Let's see how...
Cross Browser Implementation
To allow people to start programming for web workers before they're implemented across all browsers what we need is a way to emulate their behaviour in browsers that don't currently support them. Obviously there's no way to achieve the multi-threading nature on the client if it's not natively supported, but we can create our own Worker object that behaves the same as the native one in all other respects. The way we do it is as follows...
First we check for native support of the Worker object. If it is not present then we create our own Worker class.
if(typeof Worker !== 'function')
{
function Worker(sURL)
{
this.m_bReady = false;
this.m_sWorkerVariableName = '$oWorker'; //variable name by which the loaded script refers to this object
this.m_aoMessageQueue = [];
this.m_sURL = sURL;
this.m_asScripts = {};
this.m_abScriptRequests = {};
this._LoadScript(sURL);
}
In this constructor we initialise various properties which we will need and call the _LoadScript
method, which takes care of loading up
all the necessary code from source sURL
. We'll get to the implementation of this later. The worker object will also need onmessage
and onerror
methods, the user will override these functions to handle messages from the worker's code.
Worker.prototype.onmessage = function(e)
{
//the worker's script postMessage(o) calls into this.onmessage({data: o})
//override this to handle messages from the worker script
};
Worker.prototype.onerror = function(e)
{
//if the worker's script postMessage(o) throws an error, it will be caught and enter into here
//override this to handle errors from the worker script
//TODO - asynchronous events which cause an error in the
// worker script will not trigger onerror to be called
};
Our implementation will emulate messaging between this worker object and the worker source code with direct function calls. In more detail we're going to
be re-writing the worker source code in such a way that wherever it would normally call postMessage
it will now call a method directly on the
worker object - let's call it _onmessage
- which calls into the worker object's onmessage
method.
Worker.prototype._onmessage = function(oMessage)
{
this.onmessage({data: oMessage});
};
Likewise, we need to handle calls to postMessage
on the worker object itself. Calls to this method should trigger the onmessage
handler
in the worker source code. We handle this similarly to the previous case, this time we specify that postMessage
on the worker object calls another method
- let's call it _postMessage
. When we rewrite the worker source code we replace assignments to the onmessage
handler with an assignment to
the worker objects _postMessage
method instead.
Worker.prototype._postMessage = function(e)
{
};
Worker.prototype.postMessage = function(oMessage)
{
//passes messages to the worker script's self.onmessage function
//if the worker script is not yet ready then add to a message queue
if(this.m_bReady)
{
try
{
this._postMessage({data: oMessage});
}
catch(e)
{
this.onerror(e);
}
}
else
{
this.m_aoMessageQueue.push(oMessage);
}
};
Worker.prototype._FlushMessageQueue = function()
{
for(var nCursor = 0, nCount = this.m_aoMessageQueue.length; nCursor < nCount; nCursor++)
{
this.postMessage(this.m_aoMessageQueue[nCursor]);
}
this.m_aoMessageQueue.length = 0;
};
Note here that we're checking if the Worker has fully loaded up all it's scripts with the property this.m_bReady
. If the worker is not yet
ready then any postMessage
requests are added to a queue which can be flushed when loading is complete. So, on to the loading...
Worker.prototype._LoadScript = function(sURL)
{
if((this.m_abScriptRequests[sURL] !== true) &&
(this.m_asScripts[sURL] == null))
{
var pMe = this;
this.m_abScriptRequests[sURL] = true;
XHR.LoadData(sURL, true, function(sScript){pMe._OnLoadScript(sURL, sScript);}, XHR.TEXT);
}
};
The _LoadScript
method will, so long as the script hasn't already been requested, make an XMLHttpRequest for the text located at sURL
.
I'm using my own object XHR
that wraps up all the XMLHttpRequest functionality for convenience, you can implement this however you desire. When the
response is recieved the string is passed to the _OnLoadScript
method for processing.
Worker.prototype._OnLoadScript = function(sURL, sScript)
{
//find all scripts that need importing and load them
//TODO - allow the script to specify the name of the file dynamically
var pMe = this;
var reImportScripts = /importScripts\(['|"](.*)["|']\)/g;
var fnImportReplacer = function(sMatch, sURL, nOffset, sScript)
{
pMe._LoadScript(sURL);
return sMatch;
}
sScript = sScript.replace(reImportScripts, fnImportReplacer);
//replace the worker script's postMessage method with a direct call to the worker's onmessage handler
//(script.postMessage triggers worker.onmessage)
sScript = sScript.replace(/(self\.)?postMessage/g, this.m_sWorkerVariableName + '._onmessage');
//replace the workers script's onmessage handler with the worker's _postMessage method
//(worker.postMessage triggers script.onmessage)
//note, make sure we don't replace the _onmessage that we just added by checking the _
sScript = sScript.replace(/[^_](self\.)?onmessage/g, this.m_sWorkerVariableName + '._postMessage');
delete this.m_abScriptRequests[sURL];
this.m_asScripts[sURL] = sScript;
for(var sKey in this.m_abScriptRequests)
{
//still have pending requests
return;
}
//only get here when have finished
this.m_abProcessedImport = [];
var sScript = this._ProcessImport(this.m_sURL);
var fn = new Function(
this.m_sWorkerVariableName,
'try{' + sScript + '}catch(e){' + this.m_sWorkerVariableName + '.onerror(e);}'
);
fn(this);
this.m_bReady = true;
this._FlushMessageQueue();
};
Worker.prototype._ProcessImport = function(sURL)
{
if(this.m_abProcessedImport[sURL] !== true)
{
var pMe = this;
var sScript = this.m_asScripts[sURL];
//TODO - allow the script to specify the name of the file dynamically
var reImportScripts = /importScripts\(['|"](.*)["|']\)/g;
var fnImportReplacer = function(sMatch, sURL, nOffset, sScript)
{
return pMe._ProcessImport(sURL);
}
sScript = sScript.replace(reImportScripts, fnImportReplacer);
this.m_asScripts[sURL] = sScript;
this.m_abProcessedImport[sURL] = true;
}
return this.m_asScripts[sURL];
};
}
Let's step through this. Firstly we need to deal with any scripts that are imported into the script we're loading. We do this by using a regular expression
to find all calls to importScripts
, extract the script to be loaded and call _LoadScript
for it. We leave these importScripts
calls in place for later and move on.
The next step is to rewrite any postMessage
calls in this worker script, again using a regular expression. This will, for example replace something
like postMessage("hello")
with $oWorker._onmessage("hello")
.
Similarly we rewrite the onmessage
handler of this worker script. For example replacing onmessage = function(e){}
with
$oWorker._postMessage = function(e){}
.
Next we check if this is the final script we need to load (remember the importScripts
calls triggered further loading). If it is we continue to
process all the imported scripts. We start from the originally requested file and recursively call the _ProcessImport
method which replaces the
importScripts
code with the actual script itself.
So, now the processing of the script text is complete we use it to create a function fn
. The code for this function is the script text we
just created. We also specify that the function takes one argument, called $oWorker
, which explains our previous replacements using the $oWorker
variable. Finally we call this function passing this
as the $oWorker
argument, which hooks up the worker script to this worker object.
That's it, magic!
There are a couple of things still to do to improve this. Firstly an importScripts
call must take a single literal string as its argument, if there are
dynamically determined arguments, or if there is more than one argument, then the import will not be handled. Also any script errors that occur in the worker script
that were initiated asynchronously will not be caught by the onerror
handler. But despite these it's fairly functional.
So let's see this in use...
Examples
Let's first take the simplest example from the Web Worker documentation, slightly modified, a rudimentary prime number search. The script on the page is given by
var nMax = 0;
var worker = new Worker('prime_search.js');
worker.onmessage = function (event)
{
var n = event.data;
if(n > nMax)
{
nMax = n;
document.getElementById('result').innerHTML = n;
}
};
The code contained in prime_search.js run by the worker is
var n = 1;
search: while(n < 50000)
{
n += 1;
for(var i = 2; i <= Math.sqrt(n); i += 1)
{
if(n % i == 0)
{
continue search;
}
}
// found a prime!
postMessage(n);
}
This finds all primes up to 50000, outputing the highest to the interface. Try it out:
Native Web Worker support:
Generate Primes
Highest prime found: --
So this should work whether you have Native Web Worker support or not. Note that in the cases where Workers are native the calculation does no interfere with the interface, however when native support is not present the calculation is being performed on the same thread as the interface and things lock up until the calculation is complete.
Well that was pretty boring, huh? So let's quit this town and move on to a much more exciting use of web workers in my JavaScript Paint application.