Introduction to Shared Memory in JavaScript

Shared memory is an advanced feature of JavaScript, that threads (concurrently executed parts of a process) can leverage. Sharing the memory means not having the trouble of passing updated data between threads and all threads can access and update the same data in the shared memory.

Doesn’t that sound lovely? Well, almost. In this post, we’ll see how to use shared memory in JavaScript and how to decide if this is what you really want to do.

Advantages & disadvantages of shared memory

We use web workers to create threads in JavaScript. The Web Workers API allows us to create worker threads that can be used to execute code in the background so that the main thread is free to continue its execution, possibly processing UI events, ensuring no UI freeze.

Worker threads run concurrently with the main thread and each other. Such simultaneous execution of different parts of a task is time-saving. You finish quicker, but it also has its own set of problems.

Making sure that each thread gets the necessary resources and communicates with each other in a timely manner is a task in itself, where a mishap can result in a surprising outcome. Or, if one thread is changing data and another one is reading it at the same time, what do you think the other thread will see? The updated or the old data?

However, web workers are not so easy to screw up. During their communication via using messages, the data they send each other is not original but a copy, meaning they don’t share the same data. They pass copies of data to each other when needed.

But sharing is caring, and multiple threads might also need to look at the same data at the same time and change them. So, banning sharing is a big no-no. This is where the SharedArrayBuffer object comes into the picture. It will let us share binary data between multiple threads.

The SharedArrayBuffer object

Instead of passing the data copies between threads, we pass copies of the SharedArrayBuffer object. A SharedArrayBuffer object points to the memory where the data is saved.

So, even when the copies of SharedArrayBuffer are passed between threads, they all will still point to the same memory where the original data is saved. The threads, thus, can view and update the data in that same memory.

Shared Memory and Web Workers Diagram

Web workers without shared memory

To see how a web worker works without using shared memory, we create a worker thread and pass some data to it.

The index.html file holds the main script inside a <script></script> tag, as you can see it below:

const w = new Worker('worker.js');
var 	n = 9;
w.postMessage(n);

The worker.js file carries the worker script:

onmessage = (e)=>{
  console.group('[worker]');
  console.log('Data received from main thread: %i', e.data);
  console.groupEnd();
}

Using the code above, we get the following output in the console:

[worker]
Data received from main thread: 9

You can read my aforementioned post on web workers for the full code explanation of the above snippets.

For now, keep in mind that data is sent back and forth between threads using the postMessage() method. The data is received on the other side by the message event handler, as the value of the event’s data property.

Now, if we change the data will it appear updated at the receiving end? Let’s see:

const w = new Worker('worker.js');
var 	n = 9;
w.postMessage(n);
n = 1;

As expected, the data has not been updated:

[worker]
Data received from main thread: 9

Why would it be, anyway? It’s just a clone sent to the worker from the main script.

Web workers with shared memory

Now, we will use the SharedArrayBuffer object in the same example. We can create a new SharedArrayBuffer instance by using the new keyword. The constructor takes one parameter; a length value in bytes, specifying the size of the buffer.

const w = new Worker('worker.js');
buff = new SharedArrayBuffer(1);
var 	arr = new Int8Array(buff);
/* setting data */
arr[0] = 9;
/* sending the buffer (copy) to worker */
w.postMessage(buff);

Note that a SharedArrayBuffer object represents only a shared memory area. To see and change the binary data, we need to use an appropriate data structure (a TypedArray or a DataView object).

In the index.html file above, a new SharedArrayBuffer is created, with only one-byte length. Then, a new Int8Array, which is one type of TypedArray objects, is used to set the data to “9” in the provided byte space.

onmessage = (e)=>{
  var arr = new Int8Array(e.data);
  console.group('[worker]');
  console.log('Data received from main thread: %i', arr[0]);
  console.groupEnd();
}

Int8Array is also used in the worker, to view the data in the buffer.

The expected value appears in the console from the worker thread, which is exactly what we wanted:

[worker]
Data received from main thread: 9

Now, let’s update the data in the main thread to see if the change is reflected in the worker.

const w = new Worker('worker.js'),
buff = new SharedArrayBuffer(1);
var 	arr = new Int8Array(buff);
/* setting data */
arr[0] = 9;
/* sending the buffer (copy) to worker */
w.postMessage(buff);
/* changing the data */
arr[0] = 1;

And, as you can see below, the update does reflect inside the worker!

[worker]
Data received from main thread: 1

But, the code also needs to work the other way around: when the value in the worker changes at first, it also needs to be updated when it’s printed from the main thread.

In this case, our code looks like this:

onmessage = (e)=>{
  var arr = new Int8Array(e.data);
  console.group('[worker]');
  console.log('Data received from main thread: %i', arr[0]);
  console.groupEnd();
  /* changing the data */
  arr[0] = 7;
  /* posting to the main thread */
  postMessage('');
}

The data is changed in the worker and an empty message is posted to the main thread signalling that the data in the buffer has been changed and is ready for the main thread to be outputted.

const w = new Worker('worker.js'),
buff = new SharedArrayBuffer(1);
var 	arr = new Int8Array(buff);
/* setting data */
arr[0] = 9;
/* sending the buffer (copy) to worker */
w.postMessage(buff);
/* changing the data */
arr[0] = 1;
/* printing the data after the worker has changed it */
w.onmessage = (e)=>{
  console.group('[main]');
  console.log('Updated data received from worker thread: %i', arr[0]);
  console.groupEnd();
}

And, this works, too! The data in the buffer is same as the data inside the worker.

[worker]
Data received from main thread: 1
[main]
Updated data received from worker thread: 7

The value appears updated in both the cases; both the main and worker threads are viewing and changing the same data.

Final words

As I’ve mentioned earlier, using shared memory in JavaScript is not without downsides. It’s up to developers to ensure that the sequence of execution happens as predicted and no two threads are racing to get the same data because no one knows who will take the trophy.

If you are interested in shared memory more, have a look at the documentation of the Atomics object. The Atomics object can help you with some of the hardships, by reducing the unpredictable nature of reading/writing from the shared memory.

FacebookTwitterInstagramPinterestLinkedInGoogle+YoutubeRedditDribbbleBehanceGithubCodePenEmail