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:
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:
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:
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:
And to consume a worker using Deferred, we have the following helper function:
Finally, all that remains is to make a call into the $.work function to start the worker!
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:
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.