JavaScript Canvas - Web Workers
Written by Ian Elliot   
Tuesday, 07 June 2022
Article Index
JavaScript Canvas - Web Workers
The Trouble With Threads
Transferable Objects

The Trouble With Threads

If you have experience of writing multi-threaded programs, and all of the problems that this creates, this is where you might be getting worried. In general, multi-threaded programs are difficult to get right.

Starting a new thread so easily seems to be a simple way to do something dangerous. However, web workers have been implemented in a way that restricts the way that you use them to make them safe. At first these restrictions might seem tough to live with, but after a while you realize that they are perfectly reasonable and don't really stop you from doing anything.

  • The main simplification about threading with web workers is that the new thread cannot share anything with the UI thread.

The new thread cannot access any objects that the UI Thread can. This means it cannot interact with any global objects and it cannot interact with the DOM or the user interface in any way. As the canvas is a DOM object, it is out of bounds. The good news is that there is a new object, the OffscreenCanvas object. This is not part of the DOM so web workers can access it and it is very easy to transfer its contents to a canvas object which is part of the DOM. However, it is important to know that the worker thread is isolated from the UI thread and cannot access any objects that are created by the UI thread and vice versa. The new thread runs in a little world of its own, but don't panic as it can communicate with the UI thread in a very simple way.

This inability to share objects may seem a little restrictive, but it is a restriction that is necessary to make sure that the two threads you now have don't try to access the same object at the same time. If this was allowed to happen you would need to introduce a lot of complicated machinery – locks, semaphores and so on – to make sure that the access was orderly and didn't give rise to any very difficult to find bugs such as race conditions, deadlock and so on.

The problem is that if you have multiple threads accessing the same data you can't be sure of the order that things are happening in. Suppose one thread is in the middle of updating a shared object and a second thread deletes it in the middle of the update. This would at best lead to a program that didn't do what the user expected and at worst would generate a runtime error. The only way to stop this from happening is to use mechanisms that restrict access to shared objects so that only one thread can be working with it at any given time. This is potentially very complex and it is very difficult to prove that you have things correct. The alternative to managing shared objects in this way is to ban shared objects. In this case there is absolutely no danger of a simultaneous access to any object, but it makes interaction impossible.

In other words, the web worker has big restrictions so that you can use it without complication and without any danger. For most purposes, however, it is sufficient and hence very effective.

  • Web workers do have access to all of the global core JavaScript objects that you might expect, but they are not shared.

They can also access some functions that are normally associated with the DOM, including XMLHttpRequest() and setInterval.

The rule is that if the object is unique to the UI thread, or could be shared in any way with the UI thread, then you cannot get access to it and this is a condition that is obviously satisfied for all of the core JavaScript objects and the few DOM derived objects that are allowed.

To make up for this restriction there are two new objects that the web worker can access - WorkerNavigator and WorkerLocation. The navigator provides basic information about the app's context - the browser in use, appName, appVersion and so on. The location object provides details of where the app is in terms of the current URL.

If these two objects don't provide enough information you can easily arrange to pass the worker thread additional data of your choosing.

Basic Communication Methods

So if the web worker is isolated from the UI thread, how do the two communicate? The answer is that they both use events and a method that causes events on the other thread.

UI Thread to Worker Thread

Let's start with the UI thread sending a message to the worker thread. The Worker object that is created on the UI thread has a postMessage method which triggers a message event on the worker thread. Notice that this is where the thread crossover occurs. The Worker object is running on the UI thread, but the event occurs in the code running on the worker thread, which has an event queue all of its own – it is a complete event-driven program.

For example:

var worker=new Worker("myScript.js");
worker.postMessage({mydata:"some data"});

The postMessage method triggers a message event in the web worker code and sends it an event object that includes the data object as its data property. That is, event.data has a mydata property equal to "some data".

To get the message sent to the worker you have to set up an event handler and retrieve the event object's data property. For example:

this.addEventListener("message", function (event) {

In the web worker code the global context is provided by this or self and gives access to all of the methods and objects documented. To get the message you would use:

var data = event.data.mydata;

Of course, as you are passing an object to the event handler you could package as many data items as you needed to.

It is important to be very clear what is going on here. The postMessage method call is on the UI thread, but the event handler is on the worker thread. It is also important to realize that the data that is passed between the two threads isn't shared. A copy is made using the structured clone algorithm and it is this copy that the worker receives. You can use a wide range of types of data to pass to the worker, but if it is big the time taken to copy could be significant. If this is the case. you need to use a transferable object, which is shared rather than copied. As you can guess, an OffscreenCanvas is too big to move between threads in this way and it is a transferable object, as explained later.

Worker Thread to UI Thread

Passing data from the worker thread to the UI thread works in exactly the same way – only the other way round. You use the postMessage method of the DedicatedWorkerGlobalScope object in the Worker thread and attach an event handler for the message event in the UI thread.

The DedicatedWorkerGlobalScope object can be accessed using this or self. For example, in the web worker code:

this.postMessage({mydata:"some data"});

or:

self.postMessage({mydata:"some data"});

Notice that in this case you can use this or self to call postMessage because you are running inside the web worker. Use self when this is set to something else other than the web worker, i.e. when you are programming within another object.

A message event is triggered in the UI thread and you can define a handler and retrieve the data using:

worker.addEventListener("message", 
function (e) {
var somedata= e.data.mydata;
});

Once again you have to be very clear that you understand what is running where. In this case the postMessage method is running on the Worker thread and the event handler is running on the UI thread.

This is about all there is to using web workers. There are some details about error handling and terminating the thread before it is complete, but these are just details. The general idea is that you use the message event to communicate between the two threads.

There is one subtle point that is worth keeping in mind. The events that you trigger in passing data between the two threads will happen in the order that you trigger them, but they may not be handled promptly.

For example, if you start your worker thread doing an intensive calculation then triggering a "how are you doing" message event from the UI thread might not work as you expect. It could be that the worker thread is so occupied with its task that events are ignored until it reaches the end. The same happens with messages passed from the worker thread, but in this case the UI thread is generally not so focused on one task and so events usually get processed.

The rule is that UI thread events are generally handled promptly because that’s the way we tend to build UI code, but worker thread events aren't because that’s the way we tend to build worker code. That is, events going from the worker thread to the UI get processed as part of keeping the UI responsive. Events going the other way, i.e. from the UI thread to the web worker, are not so reliable. If you want to drive the worker thread using events from the UI thread you basically have to design it to be event-driven. This means writing the worker thread as an event handler that responds to the messages that the UI thread sends, and this means that your code has to give up the worker thread every so often to allow it to process events. For a general worker thread this can be difficult because you have to essentially pause the calculation it is performing and allow the thread to service the event queue. For a graphics worker thread this is much easier because generally when a frame is complete the thread is released.

To sum up:

  • Communication between the UI and worker thread is via events fired by one thread and received by the other.

  • Each thread only processes events when not occupied with running code.

  • This means that events may not be dealt with promptly.

  • Data can be transferred between threads using the event object that is made available to the event handler.

  • Data is not shared – a copy is made for the receiving thread.

  • The UI thread is generally set up to respond to events promptly, but the worker thread isn’t.

  • A graphics-oriented worker thread is the exception to the rule as it generally gives up its thread with each call to requestAnimationFrame.



Last Updated ( Tuesday, 07 June 2022 )