HTML5 Tech: Shared Web Workers Help Spread the News

By Robert Gravelle

After being a fixture in languages like Java for years, Web Workers have now made multi-threading in Web applications a reality. Right now, 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. I first wrote about them in an article entitled Introducing HTML 5 Web Workers: Bringing Multi-threading to JavaScript. In it, I spoke about dedicated Web Workers and covered the basics of their uses, limitations, as well as communication and error handling models.  In today's follow up, we're going to move on to a different kind of Web Worker called a Shared Worker.

The Difference between the Two

The shared Web Worker's main distinguishing feature is one of scope. As we saw in "Introducing HTML 5 Web Workers", Dedicated Workers are linked to the script that created them (called the owner or creator). Shared workers, on the other hand, are named so that any script running in the same origin can communicate with them, either by the URL of the script used to create it, or by name.

We're going to put that to the test by modifying the Pi calculator to use a Shared worker. As in the last article, the main web page will contain a button to call the script that instantiates the Web Worker and kick off the calculation of Pi in a separate thread. At the end of the job, the final value of Pi will be posted. A second script will display the Pi values as the calculation progresses. This script will not only reside within separate <SCRIPT> tags, but within a whole different web page on the same server and displayed with the help of an iFrame.

The Main HTML Page Markup

Since we want the Web Worker to be available from other scripts, we would now create a new SharedWorker rather than a standard Worker object. Unlike dedicaded workers, Shared Workers use a port to communicate over, so we need to bind our event listeners to the worker's port instead of directly to the worker. After binding event listeners, a call to worker.port.start() opens the channel:

<!DOCTYPE HTML>
<html>
  <head>
    <meta charset="utf-8" />
    <title>Shared Workers Test Page #1</title>
    <script>
      var worker;
			
      function start() {
        worker = new SharedWorker('pi_shared.js');
        worker.port.addEventListener("message", function(e) {  
          var data = e.data;
          var result = document.getElementById("result");
          if (data.FinalPiValue) {
            result.innerHTML += data.FinalPiValue + ' (final)';
          } else if (data.value) {
    	      console.log('Page 1 received message: '+data.value);  
          }
        }, false);  
        worker.port.start();  
        // post a message to the shared web worker  
        console.log("Calling the worker from page 1");
        worker.port.postMessage({cmd: 'connect', id: "Page 1"});
        //set the iframe page
        document.getElementById("inner").src = 'SharedWorkersTestPage2.html';
      }
    </script>
  </head>
  <body>
    <h1>Shared Workers Test Page #1</h1>
    <article>
      <input type="button" onclick="start()" value="Calculate Pi" />
      <br/>
      <br/>
      <div id="result"></div>
      <br/>
      <iframe id="inner" style="width:600px; height:300px" src=""></iframe>
    </article>
  </body>
</html>

The Inner HTML Page

As mentioned above, the purpose of the second page is to display updates on the Pi calculation. Upon loading, it adds its own event listener to the worker's port so that it can intercept messages from the worker that contain the Pi values:

<!DOCTYPE HTML>
<html>
<head>
<title>Shared Workers Test Page #2</title>
<script type="text/javascript">
  window.onload = function() {
    console.log("Calling the worker from page 2");
    parent.worker.port.addEventListener("message", function(e) {  
      var data = e.data;
      var result = document.getElementById("result");
      if (data.PiValue) {
        result.innerHTML += data.PiValue + '<br/>';
      } else if (data.value) {
        console.log('Page 2 received message: '+data.value);  
      }
    }, false);  
    parent.worker.port.start();  
  }
</script>
</head>
<body>
<h1>Shared Workers Test Page #2</h1>
<article>
  <div id="result"></div>
</article>
</body>
</html>

The Shared Worker Script

When you use shared workers, it's easy to have scripts stepping over each other's toes - too many cooks in the kitchen! Therefore it's a good idea to have the main page be the controller of the worker, while other scripts listen in. To implement that, the worker script keeps track of the connections. Shared Web Workers have a special event called "onconnect" which is called whenever a new object obtains a reference to the Shared Worker thread. Besides being the place to add your onmessage event handlers, it's also ideal to track connections. The first connection would be that of the main page.

For even more fine grained control, the worker can also keep track of the IDs of the calling scripts and assign rights and permissions accordingly, as done here using an associative array. Unfortunately, the onconnect event doesn't contain any information about the calling script, so we have to send it ourselves in the first message. After that, all messages to the worker contain an ID field to identify the caller. We don't even have to store any data in the array elements because the element keys are all that's needed. The !(data['id'] in connections) check tests for new connections in the onmessage handler

	
var connections = new Array();  
connections.length = 0;
self.addEventListener("connect", function (e) {  
    var port = e.ports[0];
    
    port.addEventListener("message", function (e) {  
      var data = e.data;
      if (!data['id']) {
        port.postMessage({value: "Please identify yourself."});
      } else {
        switch (data['cmd']) {
          case 'connect':
            if (!(data['id'] in connections)) {
              connections[data['id']] = null;
              connections.length++;
              port.postMessage({value: data['id'] + " has connected on port #" + connections.length + "."});
            }		
            port.postMessage({value: "Received cmd of '" + data['cmd'] + "' from " + data['id'] + "."});
            if (connections.length == 1) {
              port.postMessage({value: "Starting calculation of Pi."});
              CalculatePi(10000, port);
            }
            break;
        }
      }
    }, false);  
    port.start();  
}, false);

function CalculatePi(loop, port)
{
    var c = parseInt(loop);
    var f = parseFloat(loop);
    var n=1;
		
    for (var i=0,j=0,Pi=0;i<=c;i++) {
      Pi=Pi+(4/n)-(4/(n+2));
      n=n+4;
      if (++j == 1000) {
        port.postMessage({type: 'data', PiValue: Pi});
        j=0;
      }
    }
    port.postMessage({type: 'data', FinalPiValue: Pi});
}

Local Domain Woes

While testing my code, I soon learned about the "localhost" bug, whereby any attempts to create a worker from localhost or a local file fail. The exact error is:

"Uncaught Error: SECURITY_ERR: DOM Exception 18". viewing this file in the file:/// protocol or over http://? 
You'll have to serve the page in order for security to process it correctly."

The lesson here is to use an HTTP server to serve up your pages. Pretty much anything will do.

Here's what it all looks like in Google Chrome 15:

shared_worker_in_google_chrome_15

Conclusion

In my experience with Web Workers, I noticed that interacting directly with a worker that is running in a tight loop is not best if you expect anything from it in a timely manner. A better approach would be a three-tiered one where the top level is the scripts that create and interact with the worker(s). The second is a controller worker that handles access and communications. The lowest level worker performs the resource intensive processes and would only send out messages once the process has begun.



Make a Comment

Loading Comments...

  • Web Development Newsletter Signup

    Invalid email
    You have successfuly registered to our newsletter.
  •  
  •  
  •  
Thanks for your registration, follow us on our social networks to keep up-to-date