HTML5: Easily parallelize jobs using web workers and a threadpool

3 minute read

I’ve been experimenting with web workers and the various browser implementations. Most of the articles I’ve seen show an example where a single worker thread is started in the background to execute some heavy task. This frees up the main thread to render the rest of the webpage and respons to user input. In a previous article I showed how you can off-load CPU heavy tasks to a seperate web worker thread. In that example we used a couple of libraries to get the following effect:

sophie web worker

Sinc almost everyone nowadays has multiple cores it’s a waste not to use them. In this article I’ll show how we can use a simple threadpool to parallelize this even further and increase the rendering time by +/- 300%. You can run this example from the following location: http://www.smartjava.org/examples/webworkers2/

The threadpool code

To test multiple threads with web workers I wrote a simple (and very naive) threadpool / taskqueue. You can configure the maximum number of concurrent web workers when you create this pool, and any ‘task’ you submit will be executed using one of the available threads from the pool. Note that we aren’t really pooling threads, we’re just using this pool to control the number of concurrently executing web workers.

function Pool(size) {
    var _this = this;

    // set some defaults
    this.taskQueue = [];
    this.workerQueue = [];
    this.poolSize = size;

    this.addWorkerTask = function(workerTask) {
        if (_this.workerQueue.length > 0) {
            // get the worker from the front of the queue
            var workerThread = _this.workerQueue.shift();
            workerThread.run(workerTask);
        } else {
            // no free workers,
            _this.taskQueue.push(workerTask);
        }
    }

    this.init = function() {
        // create 'size' number of worker threads
        for (var i = 0 ; i < size ; i++) {
            _this.workerQueue.push(new WorkerThread(_this));
        }
    }

    this.freeWorkerThread = function(workerThread) {
        if (_this.taskQueue.length > 0) {
            // don't put back in queue, but execute next task
            var workerTask = _this.taskQueue.shift();
            workerThread.run(workerTask);
        } else {
            _this.taskQueue.push(workerThread);
        }
    }
}

// runner work tasks in the pool
function WorkerThread(parentPool) {

    var _this = this;

    this.parentPool = parentPool;
    this.workerTask = {};

    this.run = function(workerTask) {
        this.workerTask = workerTask;
        // create a new web worker
        if (this.workerTask.script!= null) {
            var worker = new Worker(workerTask.script);
            worker.addEventListener('message', dummyCallback, false);
            worker.postMessage(workerTask.startMessage);
        }
    }

    // for now assume we only get a single callback from a worker
    // which also indicates the end of this worker.
    function dummyCallback(event) {
        // pass to original callback
        _this.workerTask.callback(event);

        // we should use a seperate thread to add the worker
        _this.parentPool.freeWorkerThread(_this);
    }

}

// task to run
function WorkerTask(script, callback, msg) {

    this.script = script;
    this.callback = callback;
    this.startMessage = msg;
};

Using the threadpool

To use this threadpool we now just have to do this:

    var pool = new Pool(6);
    pool.init();

This will create a pool that will allow a maximum number of 8 threads running concurrently. If we want to create a task to be executed by this pool we just create a workerTask and submit it like this:

      var workerTask = new WorkerTask('extractMainColor.js',callback,wp);
      pool.addWorkerTask(workerTask);

This will create a web worker from ‘extractMainColor.js’ and register the supplied function as callback. Once the worker is ready to be run, the last argument will be used to send a message to the worker. A caveat on this implementation. I now assume that the when the web worker sends a message back it will close itself after sending this message. As you can see in the following example:

importScripts('quantize.js' , 'color-thief.js');

self.onmessage = function(event) {
    var wp = event.data;
    var foundColor = createPaletteFromCanvas(wp.data,wp.pixelCount, wp.colors);
    wp.result = foundColor;
    self.postMessage(wp);

    // close this worker
    self.close();
};

Results

I’ve tested this a couple of times with different settings for number of concurrent threads. The results are shown in the following table:

Chrome:

Number of threadsTotal rendering time
114213
29956
38778
47846
56924
66309
75912
85468
95201
105193
115133
125208

The result for firefox are less impressive, but you can still see a big gain:

Firefox:

Number of threadsTotal rendering time
117909
211273
310422
410154
510115
610052
710000
89997

As you can see, both for Firefox and Chrome it’s useful to not just use a single web worker, but further split the tasks. For firefox we can see a big gain if we use two web workers, and for chrome we keep on getting better results to 8 or 9 parallel web workers!

Updated: