Web Workers Part 2: Using jQuery Deferred

The .NET Task Parallel Library is a great advance in parallel programming for the .NET framework. It lets us easily run an anonymous method in another thread without any worries about the actual thread creation. A Task object wraps up a piece of parallel code, and provides a notification of when it's complete. We can use the Task.WaitAll or TaskFactory.ContinueWhenAll functions to do something after a collection of Tasks are all complete, or use the Task.WaitAny or TaskFactory.ContinueWhenAny to wait until one is complete. The ContinueWith method schedules code to be run after a single task is complete.

Hold on...isn't this blog post supposed to be about JavaScript...? Read on...

A few weeks ago I had the urge to implement the Task Parallel Library in JavaScript, so I got to work writing a generic Web Worker, created a Task class to wrap it, and implemented the TPL's ContinueWhenAny, ContinueWhenAll and ContinueWith functions, which was fun, but as my code was nearing completion, jQuery 1.5 came out with the Deferred framework included, implementing what I had done...but better!

In the remainder of this post I'm going to show you how to combine jQuery Deferred with Web Workers to create a TPL-like parallel programming environment.

Firstly a quick intro to jQuery Deferred. A Deferred object represents an asynchronous activity, and relays the success or failure of it, along with results, to any registered callbacks. It used to be the case that if you were performing an asynchronous action and wanted to make a callback at the end, you would allow the consumer to pass in a callback function. Now, you just return a Deferred object to the consumer, and call its resolve function when you want any listeners to be notified. Take this example of the jQuery 1.4 ajax function, before it used Deferred:

$.ajax({
  url: "w.php",
  success: function(result){
    //Do something with the result
  }
});

And in jQuery 1.5, that changes to the following, where "success" is no longer a simple callback - but a function on the Deferred object created by the $.ajax request:

$.ajax("w.php").success(function(result){
	//Do something with the result
});

Note that, just to confuse matters, the $.ajax request returns a specialised Deferred object which gives us the success, error and complete callback hooks for ease of use - the standard Deferred methods are implemented internally. So it's probably not the best example. Here's a lovely example where a Deferred object is created to represent the completion of an animation:

function fadeIn(selector){
	//Create a deferred object
	return $.Deferred(function(dfd) {
		//Fade in the element, on completion resolve the deferred.
		$(selector).fadeIn( 1000, dfd.resolve );
	});
}

In fact any action can be represented as a deferred object, which would be really useful because we could then chain any time-consuming actions together in a simple way.

As I mentioned at the start, I found that Deferred implemented a lot of the functionality in my JavaScript TPL implementation. Here is a comparison between TPL functions and Deferred:

TPL Deferred Description
new Task(action) $.Deferred(function) Creates a new Task or Deferred from a function.
ContinueWith(action) then(function), done(function) Creates a new Task or Deferred from a function, to be run when the current Task or Deferred is complete.
WaitAll ... Blocks the current thread until all tasks are complete. Bad idea in javascript since you'd be blocking the UI thread!
WaitAny ... Blocks the current thread until any task is complete.
TaskFactory.ContinueWhenAll $.when(function) Creates a new Task or Deferred which is run when the supplied collection of Task/Deferred objects is complete.
TaskFactory.ContinueWhenAny ... Creates a new Task which is run when any of the supplied collection of Task objects is complete.

Combining a Web Worker with Deferred

Let's firstly define a simple Web Worker object, and put it in the file worker.js:

self.onmessage = function (event) {
	var received = new Date().getTime();

	//Post the result message
	postMessage({
		received: received,
		message: event.data.message
	});
};

And to consume a worker using Deferred, we have the following helper function:

//Add a work helper function to the jQuery object
$.work = function(args) { 
	var def = $.Deferred(function(dfd) {
		var worker;
		if (window.Worker) {
			//Construct the Web Worker
			var worker = new Worker(args.file); 
			worker.onmessage = function(event) {
				//If the Worker reports success, resolve the Deferred
				dfd.resolve(event.data); 
			};
			worker.onerror = function(event) {
				//If the Worker reports an error, reject the Deferred
				dfd.reject(event); 
			};
			worker.postMessage(args.args); //Start the worker with supplied args
		} else {
			//Need to do something when the browser doesn't have Web Workers
		}
	});
	
	//Return the promise object (an "immutable" Deferred object for consumers to use)
	return def.promise(); 
};

Finally, all that remains is to make a call into the $.work function to start the worker!

//Call the helper work function with the name of the Worker file and arguments.
$.work({file: 'perf_worker.js', args: { message: "ping!" }}).then(function(data) {
	//Worker completed successfully
	console.log(data);
}).fail(function(data){
	//Worker threw an error
	console.log(data);
});

Beautiful! Now let's see an example of how Deferred makes life a lot easier now. Let's assume we've already completed the trivial task of writing a worker "primes.js" that calculates the prime numbers between a pair of values. Our task is to consume that worker and calculate the primes between 1 and 1 million. We can split that into two workers as follows:

var worker1 = $.work({file: 'primes.js', args: { from: 1, to: 500000 }});
var worker2 = $.work({file: 'primes.js', args: { from: 500001, to:1000000 }});

$.when(worker1, worker2).done(function(result1, result2){
	//All finished! Combine the results from both workers.
});

Limitations

In the code above we don't have any clear way to make it work in browsers that don't have Web Workers, and that's bad. Hopefully in the next post we can resolve that.

Also, we're running the same worker numerous times, and each time re-downloading the worker file! That's not nice. We will also look at that in the next post. But that's all for now.

MORE BY JONATHAN

Scott Logic Pool Competition

JavaScript Anonymous Functions

blog comments powered by Disqus