Tuesday, March 19, 2024

Introducing HTML 5 Web Workers: Bringing Multi-threading to JavaScript

It used to be that the only way to implement asynchronous behavior in JavaScript was to use methods like setTimeout() and setInterval(). Sometimes, the XMLHttpRequest object could be made to do the job. Event handlers are also asynchronous by nature.

Now, HTML5 is bringing us true multi-threading capability via Web Workers. These little fellas are ideal for running scripts in background threads, so that they don’t interfere with the user interface (UI). Let’s take a look at how to put Web Workers to good use.

When’s a Good Time to Use Them?

Any script that you want to execute in the background is a good candidate to run as a Web Worker. Think about cryptography, the sorting of large arrays, parallel processing, and I think you’ll see where Web Workers could be a big help.

Web Worker Limitations

Web Workers operate independently of the main browser UI thread so they’re not able to access many of its objects. One limitation is that Web Workers cannot access the DOM, so they can’t read or modify the HTML document. In addition, they can’t access any global variables or JavaScript functions within the main page. Finally, access to some objects, including the window, document, and parent, is restricted.

So what can you get at? Web Workers can access pretty much any object that is not directly related to the Document or main UI thread, including all the standard JavaScript data types, functions, and objects, such as strings, numbers, Date, Array, XMLHttpRequest, etc

An Example

There are some calculations that just never end; solving the value for PI is one of these. Although some claim to have solved it up to a billion decimal places, it still appears that the final value is nowhere in site. PI makes a great example for Web Workers for a couple of reasons. First, the calculation is simple, compared to a lot of other large numbers. The second reason is that it requires looping many, many times to get at some real accuracy, and that’s really processor intensive! Here’s the code that does the work:

var Pi=0, n=1, c=100000;
for (var i=0;i<=c;i++) {
  Pi=Pi+(4/n)-(4/(n+2));
  n=n+4;
}

I placed the code in an HTML test page to see what would happen. A text field allows me to enter a number for the loop count c variable:

<html>
<head>
<script type="text/javascript">
function CalculatePi()
{
    var loop = document.getElementById("loop");
    var c = parseInt(loop.value);
    var f = parseFloat(loop.value);
    var Pi=0, n=1;

    try {
      if (isNaN(c) || f != c ) {
        throw("errInvalidNumber");
      } else if (c<=0) {
        throw("errNegativeNumber");
      }
	
      for (var i=0;i<=c;i++) {
        Pi=Pi+(4/n)-(4/(n+2));
        n=n+4;
      }
      document.getElementById("PiValue").innerHTML = Pi;
    } catch (e) {
      var msg = "Input Error: ";
      if (e=="errInvalidNumber")
        msg += "Invalid number.";
      else if (e=="errNegativeNumber")
        msg += "Input must be positive.";
      else
        msg += e.message;
		    
        alert(msg);
    }
}
</script>
</head>
<body>
<label for="loop">Enter the number of cycles:</label>
<input id="loop" type="number" value="100" />
<input type="button" onclick="CalculatePi()" value="Calculate Pi" />
<br>
<br>
<div id="PiValue">PI value appears here</div>
</body>
</html>

I found that a number in the millions did two things: it resulted in a fairly accurate Pi value, and it slowed down the browser to a crawl! In fact, I received this message:

Slow Script dialog

Let’s try that again with a Web Worker.

The pi.js Script

So far, all I’ve done was move the CalculatePi() function into a separate script file and changed the line that sets the PiValue DIV, because the worker cannot access the DOM. Instead, I posted a message to the main thread using the code “self.postMessage({‘PiValue’: Pi});”:

function CalculatePi(loop)
{
    var c = parseInt(loop);
    var f = parseFloat(loop);
    var n=1;

    //these errors will need more work…
    if (isNaN(c) || f != c ) {
      throw("errInvalidNumber");
    } else if (c<=0) {
      throw("errNegativeNumber");
    }
	
    for (var i=0,Pi=0;i<=c;i++) {
      Pi=Pi+(4/n)-(4/(n+2));
      n=n+4;
    }
    self.postMessage({'PiValue': Pi});
}
//wait for the start 'CalculatePi' message
//e is the event and e.data contains the JSON object
self.onmessage = function(e) {
  CalculatePi(e.data.value);
}

Communicating With a Worker

The worker and the parent page communicate using messaging. Each can add a listener to the onmessage() event to receive messages from the other. As we saw in the pi.js script above, messages are sent via the postMessage() method. Depending on the browser, the message can be passed as either a string or JSON object. My advice is to stick with a JSON object as all the latest browsers that support Web Workers support it.

Back in the main page, we can add a onmessage() event handler to display the results in the DIV. There is another special event called onerror() to catch errors that are thrown from the worker:

<html>
<head>
<script type="text/javascript">
  function launchPiWebWorker() {
    var worker = new Worker('pi.js');
		
    worker.onmessage = function(e) {
      document.getElementById("PiValue").innerHTML = e.data.PiValue;
    };
    worker.onerror = function(e) {
      alert('Error: Line ' + e.lineno + ' in ' + e.filename + ': ' + e.message);
    };

    //start the worker
    worker.postMessage({'cmd':   'CalculatePi', 
                        'value': document.getElementById("loop").value
                      });
  }

</script>
</head>
<body>
<label for="loop">Enter the number of cycles:</label>
<input id="loop" type="number" value="100" />
<input type="button" onclick="launchPiWebWorker()" value="Calculate Pi" />
<br>
<br>
<div id="PiValue">PI value appears here</div>
</body>
</html>

More on Error Handling

It should be noted that the onerror() event only applies to native JavaScript errors and does NOT work for user-defined errors. There is only one means of communication between the worker and main thread available to us and that is the messaging interface. Hence, if we perform validation on the loop field value in the worker, then we must either handle the errors there as well, or use postMessage() to send information about the error to the main page. The first choice is rarely a good one, as neither the DOM or alert() method are accessible to the worker, which leaves us with option two. We can implement validation handling by adding a new “error” message type (the other being “PiValue”) and the error codes:

var c = parseInt(loop);
var f = parseFloat(loop);
var n=1;

if (isNaN(c) || f != c ) {
    postMessage({'type': 'error', 'code': 'errInvalidNumber'});
    return;
} else if (c<=0) {
    postMessage({'type': 'error', 'code': 'errNegativeNumber'});
    return;
}
//...at the end of the CalculatePi() function
//we have to add the 'data' message type to differenciate 
//from errors
self.postMessage({'type': 'data', 'PiValue': Pi});

Remember that, since at no time are we actually catching our validation errors, we must exit the function or the script will proceed to attempt to calculate Pi based on the loop field’s faulty value!

Here is the updated onmessage() event handler code to display both validation errors (in an alert) and the calculated PI value (in the “PiValue” DIV:

worker.onmessage = function(e) {
  var data = e.data;
  switch (data.type) {
    case 'error':
      var msg = 'Input Error: '
      switch (data.code) {
        case 'errInvalidNumber':
          msg += 'Invalid number.';
          break;
        case 'errNegativeNumber':
          msg += 'Input must be positive.';
          break;
      }
      alert(msg);
      break;
    case 'data':
      document.getElementById("PiValue").innerHTML = data.PiValue;
      break;
  }
};

Conclusion

After being a fixture in languages like Java for years, Web Workers have now made multi-threading in Web applications a reality. They are supported as of Opera 10.6, Safari 4.0, Chrome 11.0, Firefox 4.0 and are expected to be included in IE 10. But note that multi-threaded processes are difficult to debug and can cause some wacky behavior if your’re not careful, so proceed accordingly.

Robert Gravelle
Robert Gravelle
Rob Gravelle resides in Ottawa, Canada, and has been an IT guru for over 20 years. In that time, Rob has built systems for intelligence-related organizations such as Canada Border Services and various commercial businesses. In his spare time, Rob has become an accomplished music artist with several CDs and digital releases to his credit.

Get the Free Newsletter!

Subscribe to Developer Insider for top news, trends & analysis

Popular Articles

Featured