How to Implement JavaScript Threading with Web Workers

Screen Shot 2014-02-26 at 1.40.08 PM

CraigBucklerBy: Craig Buckler

Your web browser and the web page you’re viewing share a single processing thread. When an event occurs within the page or browser interface, the handler is added to a single queue. The next time the browser becomes idle, the next handler on the queue is processed.

In essence, the browser cannot do anything else when your JavaScript code is running; application menus and toolbars become inactive. Admittedly, it’s not quite that simple – browsers such as IE and Chrome start new operating system processes for every tab you open – but the single thread concept remains. It’s essential because JavaScript can interrupt browser processing and make changes to the HTML before, during or after the page has downloaded.

Browsers will not permit your script to run for too long since it prevents the user interacting with the application. Problems can therefore occur if you have a long-running or process-intensive task. Consider this simple example:

<!DOCTYPE html>
<html lang=”en”>
<head>
<meta charset=”UTF-8″ />
<title>processing test</title>
</head>
<body>
<p id=”output”>Processing&hellip;</p>
<script>
window.onload = function() {
var       r = 999999999, start = new Date();
while (r– > 0);
document.getElementById(“output”).textContent = “Processing took ” + (+new Date() – start) + “ms”;
}
</script>
</body>
</html>

It performs one billion simple operations but is unlikely to get that far…

How to Browsers Detect Unresponsive Scripts?

The vendors use different methods to assess browser responsiveness:

IE limits execution to a number statements

Firefox and Safari limit execution to a number of seconds

Chrome and Opera 15+ detect when the browser becomes unresponsive

You cannot accurately determine when an “unresponsive script” error will be fired because computer processing speeds, operating systems and browsers vary. I would suggest any code which runs for more than a second on an average PC is at risk.

Application Refactoring

Ideally, web-based applications should not use long-running scripts. Web pages are event-driven; the user performs an action and the client-side code reacts. If your application requires intensive processes, there may be a better solution such as pre-calculation or offloading to the server. If that is not an option, you may need to consider threading.

What is Threading?

A thread is a separate process normally managed by your operating system. Modern OSs are inherently multi-threaded since you can run more than one application at a time. Each program can perform its own tasks without waiting for another to stop.

Multi-core processors can run more than one thread concurrently but hundreds of threads could be required. Therefore the OS switches between different processes; a thread is executed for a short time before switching to the next one.

Web Workers

Web Workers are a threading technology available in HTML5. If you normally develop desktop applications, you should be aware that JavaScript imposes rigorously-controlled restrictions:

Web Worker code must be available on the same origin (domain) as the parent process which calls them.

Web Worker threads operate independently of the browser UI; they cannot access or modify the page DOM.

Web Workers cannot access global variables or JavaScript functions within your main page.

Access to some browser objects is restricted, e.g. window.location properties are read-only.

Web Workers may only communicate with your main page by receiving and returning a single parameter – although that can be an object or an array containing many items.

Web Workers are event-driven; you must write code to respond to asynchronous events in both your calling application and the Web Worker code

Web Workers are supported in Firefox, Chrome, Safari, Opera and IE10+.

We’ll re-write our large loop program to use Web Workers:

<!DOCTYPE html>
<html lang=”en”>
<head>
<meta charset=”UTF-8″ />
<title>HTML5 Web Workers</title>
</head>
<body>
<p id=”output”>Processing&hellip;</p>
<script>
window.onload = function() {
var output = document.getElementById(“output”);
if (window.Worker) {
// load web worker
var thread = new Worker(“webworker.js”);
// message received from web worker
thread.onmessage = function(e) {
output.textContent = e.data;
}
// start web worker
thread.postMessage(999999999);
}
else {
output.textContent = “Web Workers are not supported in your browser”;
}
};
</script>
</body>
</html>

This code performs the following actions:

First, we check if Web Workers are supported in the browser by checking for the existence of the window.Worker object.

The Web Worker thread code is then loaded from a separate JavaScript file using new Worker(filename).

A new handler function is defined for the onmessage event. This is executed when the Web Worker completes its work and returns an object, e. The data property is the returned message.

Finally, we start the Web Worker thread with the postMessage method. In this code, we’re passing the size of the loop as an argument.

Our Web Worker code is now defined in webworker.js (the filename we used above):

// web worker
self.onmessage = function(e)
var r = e.data,
start = new Date(),
msg = “Processing ” + r + ” items took “;
while (r– > 0);
self.postMessage(msg + (+new Date() – start) + “ms”);
};

This implements a single event handler which waits for a message sent from the calling application when it executes the postMessage method. The function:

•           is passed a message object, e, which contains our parameter (999999999) in the data property.

The loop then runs within the background process.

Finally, we can return the processing result to the calling application using another postMessage method call. This runs the onmessage handler function defined in the page above.

You will not experience any “unresponsive script” errors because webworker.js runs as a background process. As a bonus, the script generally executes faster because the browser runs it in a more limited context which is not able to communicate with the DOM.

There are a number of further points you should note about Web Workers:

Web Workers can import other scripts using the importScripts(“filename”) command. Multiple files can be imported by separating each name string with a comma.

You can spawn any number of Web Workers; each can spawn any number of sub-workers and so on. The script URI for sub-workers is relative to the parent worker’s location rather than the root page.

If you need to stop a Web Worker from your main page, you can use the terminate() method, e.g. thread.terminate();

Web Workers may terminate themselves using the close() method, e.g. self.close();

If an error occurs within a Web Worker, an onerror handler is called and passed an error event object which exposes properties including filename, message and lineno, e.g.

thread.onerror = function(e) {
console.log(“Error in file: ” + e.filename + “on line: ” + e.lineno + “: ” + e.message);
};

Pseudo-Threading

Web Workers are not supported in older browsers. While shims are available, they only implement the Web Worker API and cannot execute background processes. “Unresponsive Script” errors could still occur.

As an alternative, you could consider using JavaScript timers such as setTimeout and setInterval, e.g.

var timer = setTimeout(myfunction, 50);

This runs myfunction() after approximately 50 milliseconds but, importantly, the browser is freed to perform other tasks during that time.

If we need to run a series of process-intensive functions, we can add a delay between them, e.g.

var timer1 = setTimeout(myfunction1, 50);
var timer2 = setTimeout(myfunction2, 100);
var timer3 = setTimeout(myfunction3, 150);

However, these functions may not be called in the order you expect. To rectify that issue, we can create a function to handle calling in sequence:

function ProcessThread(func) {
// delay in milliseconds between calls
var ms = 50;
setTimeout(function() {
// call next function
func.shift()();
// further functions to execute?
if (func) {
setTimeout(arguments.callee, ms);
}
}, ms);
}
// pass array of functions
ProcessThread([myfunction1, myfunction2, myfunction3]);

Using timers is not perfect since a single function could result in an “Unresponsive Script” error, however, this may be the best option if support for older browsers is essential.

 Check out more courses and tutorials:

For more tutorials about Web Workers and other HTML5 APIs, try my Learning JavaScript Programming video course.

Comments

comments

Save $50 when you sign up for an annual Learning Library account Learn More