Moving data in our application

As we have seen in the worker_thread module inside of Node.js, there is a way to communicate with our workers. This is through the postMessage system. If we take a look at the method signature, we will see that it requires a message that can be any JavaScript object, even those with cyclical references. We also see another parameter called transfer. We will go into depth on that in a bit but, as the name suggests, it allows us to actually transfer the data instead of copying the data to the worker. This is a much faster mechanism for transferring data, but there are some caveats when utilizing it that we will discuss later.

Let's take the example that we have been building on and respond to messages sent from the frontend:

  1. We will swap out creating a new worker each time a change event occurs and just create one right away. Then, on a change event, we will send the data to the worker via the postMessage:
const dedicated_worker = new Worker('worker.js', {name : 'heavy lifter'});
document.querySelector("#in").addEventListener('change', (ev) => {
dedicated_worker.postMessage(parseInt(ev.target.value));
});
  1. If we now tried this example, we would not receive anything from the main thread. We have to respond to the onmessage event that comes on the worker's global descriptor called self. Let's go ahead and add our handler to that and also remove the self.close() method since we want to keep this around:
function calculatePrimes(val) {
let numForPrimes = val;
const primes = [];
while( numForPrimes % 2 === 0 ) {
primes.push(2);
numForPrimes /= 2;
}
for(let i = 3; i <= Math.sqrt(numForPrimes); i+=2) {
while( numForPrimes % i === 0 ) {
primes.push(i);
numForPrimes /= i;
}
}
if( numForPrimes > 2 ) {
primes.push(numForPrimes);
}
return primes;
}
self.onmessage = function(ev) {
console.log('our primes are: ', calculatePrimes(ev.data).join(' '));
}

As we can see from this example, we have moved the calculation of the primes to a separate function and when we get a message, we grab the data and pass it to the calculatePrimes method. Now, we are working with the messaging system. Let's go ahead and add another feature to our example. Instead of printing to the console, let's give the user some feedback based on what they input:

  1. We will add a paragraph tag below the input that will hold our answer:
<p>The primes for the number is: <span id="answer"></span></p>
<script type="text/javascript">
const answer = document.querySelector('#answer');
// previous code here
</script>
  1. Now, we will add to the onmessage handler of worker, just like we did inside of the worker, to listen for events from the worker. When we get some data, we will populate the answer with the values returned:
dedicated_worker.onmessage = function(ev) {
answer.innerText = ev.data;
}
  1. Finally, we will change our worker code to send the data utilizing the postMessage method to send the primes back to the main thread:
self.onmessage = function(ev) {
postMessage(calculatePrimes(ev.data).join(' '));
}

This also showcases that we do not need to add the self piece to call methods that are on the global scope. Just like the window is the global scope for the main thread, self is the global scope for the worker threads.

With this example, we have explored the postMessage method and seen how we can send data between a worker to the thread that spawned it, but what if we had multiple tabs that we wanted to communicate with? What if we had multiple workers that we wanted to send messages to?

One way of dealing with this is to just keep track of all of the workers and loop through them, sending the data out like the following:

const workers = [];
for(let i = 0; i < 5; i++) {
const worker = new Worker('test.js', {name : `worker${i}`});
workers.push(worker);
}
document.querySelector("#in").addEventListener('change', (ev) => {
for(let i = 0; i < workers.length; i++) {
workers[i].postMessage(ev.target.value);
}
});

In the test.js file, we just console log the message and say which worker we are referencing by name. This can easily get out of hand since we would need to keep track of which workers are still alive and which ones have been removed. Another way of handling this would be to broadcast the data out on a channel. Luckily, we have an API for that, called the BroadcastChannel API.

As the document on the MDN site states (https://developer.mozilla.org/en-US/docs/Web/API/Broadcast_Channel_API), all we need to do is create a BroadcastChannel object by passing a single argument into its constructor, the name of the channel. Whoever calls it first creates the channel and then anyone can listen in on it. Sending and receiving data is as simple as our postMessage and onmessage examples have been. The following takes our previous code for our test interface and, instead of needing to keep track of all the workers, just broadcasts the data out:

const channel = new BroadcastChannel('workers');
document.querySelector("#in").addEventListener('change', (ev) => {
channel.postMessage(ev.target.value);
});

Then, in our workers, all we need to do is listen in on BroadcastChannel instead of listening in on our own message handler:

const channel = new BroadcastChannel('workers');
channel.onmessage = function(ev) {
console.log(ev.data, 'was received by', name);
}

We have now simplified the process of sending and receiving a message between multiple workers and even multiple tabs that have the same host. What makes this system great is that we can have some workers based on some criteria listen in on one channel and others listen in on another. We could then have a global channel to send commands that any of them could respond to. Let's go ahead and make a simple adjustment to our primes program. Instead of sending the data to a single dedicated worker, we will have four workers; two of them will handle even numbers and the other two will handle odd numbers:

  1. We update our main code to launch four workers. We will name them based on whether the number is even or not:
for(let i = 0; i < 4; i++) {
const worker = new Worker('worker.js',
{name : `worker ${i % 2 === 0 ? 'even' : 'odd'}`}
);
}
  1. We change what happens upon an input, sending the even numbers to the even channel and the odd numbers to the odd channel:
document.querySelector("#in").addEventListener('change', (ev) => {
const value = parseInt(ev.target.value);
if( value % 2 === 0 ) {
even_channel.postMessage(value);
} else {
odd_channel.postMessage(value);
}
});
  1. We create three channels: one for the even numbers, one for the odd numbers, and one for a global send to all workers:
const even_channel = new BroadcastChannel('even');
const odd_channel = new BroadcastChannel('odd');
const global = new BroadcastChannel('global');
  1. We add a new button to kill all of the workers and hook it up to broadcast on the global channel:
<button id="quit">Stop Workers</button>
<script type="text/javascript">
document.querySelector('#quit').addEventListener('click', (ev) => {
global.postMessage('quit');
});
</script>

  1. We change our worker to handle messages based on its name:
const mainChannelName = name.includes("odd") ? "odd" : "even";
const mainChannel = new BroadcastChannel(mainChannelName);
  1. When we do get a message on one of these channels, we respond just like we have been:
mainChannel.onmessage = function(ev) {
if( typeof ev.data === 'number' )
this.postMessage(calculatePrimes(ev.data));
}
  1. If we receive a message on the global channel, we check to see whether it is the quit message. If it is, then kill the worker:
const globalChannel = new BroadcastChannel('global');
globalChannel.onmessage = function(ev) {
if( ev.data === 'quit' ) {
close();
}
}
  1. Now, back on the main thread, we will listen in on the even and odd channels for data. When there is data, we handle it almost exactly like before:
even_channel.onmessage = function(ev) {
if( typeof ev.data === 'object' ) {
answer.innerText = ev.data.join(' ');
}
}
odd_channel.onmessage= function(ev) {
if( typeof ev.data === 'object' ) {
answer.innerText = ev.data.join(' ');
}
}

One thing to note is how our workers and the main thread handle data coming in on the odd and even channels. Since we are broadcasting, we need to make sure it is the data that we want. In the case of the workers, we only want numbers and, in the case of our main thread, we only want to see arrays.

The BroadcastChannel API only works with the same origin. This means that we cannot communicate between two different sites, only with pages under the domain.

While this is an overly complex example of the BroadcastChannel mechanism, it should showcase how we can easily decouple workers from their parents and make them easy to send data to without looping through them. Now, we will return to the postMessage method and look at that transferrable property and what it means for sending and receiving data.

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

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