JavaScript Async - Advanced Worker Threads
Written by Ian Elliot   
Monday, 16 July 2018
Article Index
JavaScript Async - Advanced Worker Threads
Running a worker without a separate file
Shared and Service Workers

Worker threads are increasingly important in modern websites as well as web apps because they allow you to do long running tasks without blocking the UI thread. However they don't, as many believe, relieve you of the need to master asynchronous programming. Workers are async too.

 

This is an extract from the recently published JavaScript Async: Events Callbacks, Promises & Async/Await.

It assumes that you know the basics of how to create and run a worker thread which is is covered earlier in the chapter.

Now Available as a Print Book:

 JavaScript Async

cover

You can buy it from: Amazon

Contents

  1. Modern JavaScript (Book Only)
  2. Events,Standard & Custom
  3. The Callback 
  4. Custom Async - setTimeout, sendMessage & yield
      extract -Custom Async***NEW
  5. Worker Threads
       extract - Advanced Worker Threads 
  6. Consuming Promises 
  7. Producing Promises
       extract - The Revealing Constructor Pattern
  8. The Dispatch Queue
       extract - Microtasks
  9. Async & Await
       extract -  Basic Async & Await
  10. Fetch, Cache & Service Worker
       extract - Fetch   

Controlling The Worker

The fundamental problem in making good use of Worker threads is that they make use of an event-driven architecture just like the UI thread, but they are far less suited to it.

Sometimes events are natural for the task. For example, a Worker thread that manages downloads or database interactions is going to be idle waiting for the task to complete for enough of the time for the thread to service the event queue. Other computationally intensive tasks are less suited to events.

If you are familiar with other languages you may have used commands to start, stop and put the thread to sleep for a given number of milliseconds. There are no such features in JavaScript because the additional threads that you create are always event driven. You do not pause or sleep a Worker thread because if you don't have something for it to do then it looks after the event queue i.e. it runs its own dispatcher.

The closest you can get to pausing the thread is to make a call to setInterval to restart a function after a few milliseconds and then perform a return to free the thread.

This is of course exactly the technique we would use for the UI thread.

You can stop the thread, however, and this is about the only control you have and it is a very coarse one as it stops the thread without any warning or time to finish what it is doing. To stop a Worker thread in this way all you have to do is call the terminate method on the Worker object:

worker.terminate();

What if the Worker thread wants to terminate itself?

You might think that executing a return or just finishing the main program of the Worker thread would be enough to stop it, but of course it doesn't. It simply frees the thread to service the event queue.

If you want to terminate the thread you have to use the close method:

close();

This places the thread into a "close mode" where it doesn't respond to any events. Your code continues to run until the thread is freed. The previous example (earlier in the chapter) should have terminated the thread:

this.postMessage(state);
close();

The only other sort of control that is available is to respond to an error condition. If the Worker thread throws an exception then it has a chance to handle its own error event. If it doesn't then the error event is passed to the thread that created the worker where it can be handled and canceled.

The Async Worker Thread

If you want to control a Worker thread more finely than terminate allows or if you want to sent the Worker thread state messages you have no choice but to make it fully asynchronous.

This means treat it like the UI thread and never block the thread or monopolize it.

If you see the Worker thread as your way of being free from the concerns of asynchronous code then you are going to be disappointed.

If you want your Worker thread to be responsive to the outside world e.g. the UI thread, then you have to implement asynchronous code and specifically you have to give up the thread at regular intervals so that the event queue can be serviced and messages from the UI thread or from the timer can be handled.

What this means in practice is that you have to break up any long running function in the Worker thread. You can use yield to save the state of the function as a browser that supports Worker threads is likely to support generators, but you can't use the postMessage window method because there is no window object in a Worker thread. This means you need to use setTimeout to restart the function.

For example, let's change the Pi computation (introduced earlier) so that the UI thread can start it, stop it and ask for an update. Notice that in this case it is the UI thread that asks for the update and not the Worker thread that decides to supply one.

First we need to implement the genComputPi function, a function that creates a generator, which is exactly the same as the one used in the UI thread (earlier in the chapter) when yield was used to save the state:

You can see how all of these code fragments fit together in the complete listing.

function* genComputePi() {
  var k;
  var pi = 0;
  for (k = 1; k <= 1000000; k++) {
   pi += 4 * Math.pow(-1, k + 1) / (2 * k - 1);
   if (Math.trunc(k / 1000) * 1000 === k)
          yield pi;
  }
  return pi;
}

The computePiAsync is a little different in that it now has to restart the computePi function or post the final value back to the UI thread when the computation is complete:

function computePiAsync() {
  var computePi = genComputePi();
  function resume() {
   pi = computePi.next();
   if (!pi.done)
          setTimeout(resume, 0);
   if (pi.done)
          postMessage(pi);
   return;
  }
  setTimeout(resume, 0);
  return;
}

We need a controller function to respond to the message events from the UI Thread:

var pi;
this.addEventListener("message", function (event) {
  switch (event.data) {
   case "start":
    computePiAsync();
    break;
   case "update":
    postMessage(pi);
    break;
   case "stop":
    close();
    break;
  }
});

This controller function is called whenever the UI thread sends the worker a message with a string Object. It then works out what the message is and performs the action.

If the string is "start" it runs the computation which releases the thread every 1000 iterations so that the event queue is given a chance to process another message.

If the string is “update" then it simply posts a message back with the current value of pi. Notice that for this to work pi has to be global. An alternative to making pi global is to wrap the entire code in a function which is immediately executed, meaning any variable will be made available to all of the functions because of closure.

Finally if the string is "stop" it stops the Worker thread. In general the response to stop could be more sophisticated and include preserving state before the thread was terminated.

The UI thread now has to "drive" the Worker thread by posting messages to it. First it has to set things up including the event handler for the response to the update command:

var worker = new Worker("pi.js");
  worker.addEventListener("message",
   function (event) {
    result.innerHTML=event.data.value;
   }
);

To control the Worker thread we now have to send some messages:

worker.postMessage("start");
  setInterval(function(){
     worker.postMessage("update");
  },100);

The start message sets the computation off and every 100 milliseconds an update is requested. Notice that if you ask for an update more often than the computation yields then the update events accumulate. All of the pending updates will be performed before the calculation resumes, but the repeats are going to be a waste of time. It is important not to issue messages much faster than the receiving event queue can deal with them.

The general idea is the same for all Worker threads that need to be controlled from the UI thread.

  1. Make sure that all long running functions in the Worker thread yield frequently.

  2. Make sure that all functions in the Worker thread that yield are restarted using setTimeout or similar.

  3. Write a controller function that responds to the message event in the Worker thread that determines what to do next.

  4. The controlling thread has to send message to the Worker thread to make it do what it has to.

<ASIN:1871962560>

<ASIN:1871962579>

 <ASIN:1871962528>

<ASIN:1871962501>



Last Updated ( Monday, 16 July 2018 )