Sharing data and workers

While most of the time we want to keep the boundaries up between our workers and tabs of our applications, there will be times when we want to just share the data or even the worker among every instance. When this is the case, we can utilize two systems, SharedWorker and SharedArrayBuffer.

SharedWorker is just what it sounds like, when one spins up, just like BroadcastChannel, and someone else makes the same call to create a SharedWorker, it will just connect to the already created instance. Let's go ahead and do just this:

  1. We will create a new file for the SharedWorker JavaScript code. Inside of here, put some general computing functions such as adding and subtracting:
const add = function(a, b) {
return a + b;
}
const mult = function(a, b) {
return a * b;
}
const divide = function(a, b) {
return a / b;
}
const remainder = function(a, b) {
return a % b;
}
  1. Inside of one of our current workers' code, start up SharedWorker:
const shared = new SharedWorker('shared.js');
shared.port.onmessage = function(ev) {
console.log('message', ev);
}

We will already see a problem. Our system states that SharedWorker is not found. To utilize SharedWorker, we have to start it in a window. So now, we will have to move that start code to our main page.

  1. Move the start code into the main page and then pass the port to one of the workers:
const shared = new SharedWorker('shared.js');
shared.port.start();
for(let i = 0; i < 4; i++) {
const worker = new Worker('worker.js',
{name : `worker ${i % 2 === 0 ? 'even' : 'odd'}`}
);
worker.postMessage(shared.port, [shared.port]);
}

We now run into another problem. Since we wanted to pass the port to the worker and not have access to it in the main window, we utilized the transferrable system. However, since we only had a single reference at that time, once we send it to one worker, we can't send it again. Instead, let's start one worker and turn our BroadcastChannel system off.

  1. Comment out our BroadcastChannels and all of our looping code. Let's only start a single worker up in this window:
const shared = new SharedWorker('shared.js');
shared.port.start();
const worker = new Worker('worker.js');
document.querySelector("#in").addEventListener('change', (ev) => {
const value = parseInt(ev.target.value);
worker.postMessage(value);
});
document.querySelector('#quit').addEventListener('click', (ev) => {
worker.postMesasge('quit');
});

  1. With these changes, we will have to simplify our dedicated worker. We will just respond to events on our message channel like before:
let sharedPort = null;
onmessage = function(ev) {
const data = ev.data;
if( typeof data === 'string' ) {
return close();
}
if( typeof data === 'number' ) {
const result = calculatePrimes(data);
const send = new Int32Array(result);
return postMessage(send, [send.buffer]);
}
// handle the port
sharedPort = data;
}
  1. Now we have the SharedWorker port in a single worker, but what did all of this solve for us? Now, we can have multiple tabs open at the same time and get the data to every single one of them. To see this, let's hook a handler up to sharedPort:
sharedPort.onmessage = function(ev) {
console.log('data', ev.data);
}
  1. Finally, we can update our SharedWorker to respond once a connection happens, like the following:
onconnect = function(e) {
let port = e.ports[0];
console.log('port', port);
port.onmessage = function(e) {
port.postMessage('you sent data');
}
port.postMessage('you connected');
}

With this, we will see a message come back to our workers. We now have our SharedWorker up and running and communicating directly with our DedicatedWorker! However, there is still one problem: why did we not see the log from our SharedWorker? Well, our SharedWorker lives in a different context than our DedicatedWorker and our main thread. To get access to our SharedWorker, we can go to the URL chrome://inspect/#workers and then locate it. Right now, we did not call it anything so it should be called untitled, but when we click the inspect option underneath it, we now have a debug context for the worker.

We have connected our SharedWorker to the DOM context, and we have connected every DedicatedWorker to that SharedWorker, but we need to be able to send messages to each DedicatedWorker. Let's go ahead and add this code:

  1. First, we will need to keep track of all of the workers that connected to us through the SharedWorker. Add the following code to the bottom of our onconnect listener:
ports.push(port);
  1. Now, we will add some HTML to our document so we can send the add, multiply, divide, and subtract requests along with two new number inputs:
<input id="in1" type="number" />
<input id="in2" type="number" />
<button id="add">Add</button>
<button id="subtract">Subtract</button>
<button id="multiply">Multiply</button>
<button id="divide">Divide</button>
  1. Next, we will pass this information through the DedicatedWorker to the SharedWorker:

if( typeof data === 'string' ) {
if( data === 'quit' ) {
close();
} else {
sharedPort.postMessage(data);
}
}

  1. Finally, our SharedWorker will run the corresponding operation and pass it back to the DedicatedWorker, which will log the data to the console:
port.onmessage = function(e) {
const _d = e.data.split(' ');
const in1 = parseInt(_d[1]);
const in2 = parseInt(_d[2]);
switch(_d[0]) {
case 'add': {
port.postMessage(add(in1, in2));
break;
}
// other operations removed since they are the same thing
}
}

With all of this, we can now have multiple tabs of our application open that are all sharing the same preceding math system! This is overkill for this type of application, but it could be useful when we need to perform complex operations in our application that span multiple windows or tabs. This could be something that utilizes the GPU and we only want to do this once. Let's go ahead and wrap this section up with an overview of SharedArrayBuffer. However, one thing to remember is that a SharedWorker is a single thread held by all tabs, whereas a DedicatedWorker is a thread per tab/window. While sharing a worker can be beneficial for some tasks explained previously, it can also slow down other tasks if multiple tabs are utilizing it at the same time.

SharedArrayBuffer allows all of our instances to share the same block of memory. Just as a transferrable object can have different owners based on passing the memory to another worker, a SharedArrayBuffer allows different contexts to share the same piece. This allows for updates to propagate across all of our instances and has almost instant updates for some types of data, but it also has many pitfalls associated with it.

This is as close as we will most likely get to SharedMemory in other languages. To properly utilize SharedArrayBuffer, we will need to utilize the Atomics API. Again, not diving directly into the detail behind the Atomics API, it makes sure that operations happen in the correct sequence and that they are guaranteed to update what they need to without anyone overriding them during their update.

Again, we are starting to get into details where it can be hard to fully understand what is happening. One good way to think of the Atomics API is a system where many people are sharing a piece of paper. They all take turns writing on it and reading what others wrote down.

However, one of the downfalls is that they are only allowed to write a single character at a time. Because of this, someone else may write something in their location while they are still trying to finish writing their word, or someone may read their incomplete phrase. We need a mechanism for people to be able to write the entire word that they want, or read the entire section, before someone starts writing. This is the job of the Atomics API.

SharedArrayBuffer does suffer from issues related to browsers not supporting it (currently, only Chrome supports it without a flag), to issues where we might want to use the Atomics API (SharedWorker cannot send it to the main thread or the dedicated workers due to security issues).

To set up a basic example of SharedArrayBuffer in action, we will share a buffer between the main thread and a worker. When we send a request to the worker, we will update the number that is inside that worker by one. Updating this number should be visible to the main thread since they are sharing the buffer:

  1. Create a simple worker and using the onmessage handler check whether it received a number or not. If it is, we will increment the data in the SharedArrayBuffer. Otherwise, the data is the SharedArrayBuffer coming from the main thread:
let sharedPort = null;
let buf = null;
onmessage = function(ev) {
const data = ev.data;
if( typeof data === 'number' ) {
Atomics.add(buf, 0, 1);
} else {
buf = new Int32Array(ev.data);
}
}

  1. Next, on our main thread, we are going to add a new button that says Increment. When this is clicked, it will send a message to the dedicated worker to increment the current number:
// HTML
<button id="increment">Increment</button>
<p id="num"></p>

// JavaScript
document.querySelector('#increment').addEventListener('click', () => {
worker.postMessage(1);
});
  1. Now, when the worker updates the buffer on its side, we will constantly be checking SharedArrayBuffer if there is an update. We will always just put the number inside of the number paragraph element that we showed in the previous code snippet:
setInterval(() => {
document.querySelector('#num').innerText = shared;
}, 100);
  1. Finally, to kick all of this off, we will create a SharedArrayBuffer on the main thread and send it to the worker once we have launched it:
let shared = new SharedArrayBuffer(4);
const worker = new Worker('worker_to_shared.js');
worker.postMessage(shared);
shared = new Int32Array(shared);

With this, we can see that our value is now incrementing even though we are not sending any data from the worker to the main thread! This is the power of shared memory. Now, as stated previously, we are quite limited with the Atomics API since we cannot use the wait and notify systems on the main thread and we cannot use SharedArrayBuffer inside of a SharedWorker, but it can be useful for systems that are only reading data.

In these cases, we may update the SharedArrayBuffer and then send a message to the main thread that we updated it, or it may already be a Web API that takes SharedArrayBuffers such as the WebGL rendering context. While the preceding example is not very useful, it does showcase how we might be able to use the shared system in the future if the ability to spawn and use SharedArrayBuffer in a SharedWorker is available again. Next, we will focus on building a singular cache that all the workers can share.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.144.222.185