Communicating between Web Workers via MessageChannel
Occasionally, you want Web Workers to communicate with each other. Doing so is not obvious as most Web Worker examples are about communicating between the main thread and a Web Worker. There, one uses postMessage()
to send messages directly to the Worker. Alas, that doesn’t work for communicating between two Workers, because you can’t pass references to Workers around.
MessageChannel
The solution is to establish a channel between the Workers:
const channel = new MessageChannel();
receivingWorker.postMessage({port: channel.port1}, [channel.port1]);
sendingWorker.postMessage({port: channel.port2}, [channel.port2]);
We are creating a new MessageChannel
and sending references to its two ports to two Workers. Every port can both send and receive messages. Note the second parameter of postMessage()
: It specifies that channel.port1
and channel.port2
should be transfered (not copied) to the Workers. We can do that because MessagePort
implements the interface Transferable
.
Posting messages
sendingWorker
posts messages and looks as follows:
const sendingWorker = createWorker(() => {
self.addEventListener('message', function (e) { // (A)
const {port} = e.data; // (B)
port.postMessage(['hello', 'world']); // (C)
});
});
The tool function createWorker()
for inlining Worker code is explained later.
The Worker performs the following steps:
- Line A: Listen to messages sent to you from the main thread.
- Line B: The first and only such message is an object whose property
port
holds theMessagePort
. - Line C: Send data over the channel, via
port
.
Receiving messages
receivingWorker
first receives its port and then uses it to receive messages:
const receivingWorker = createWorker(() => {
self.addEventListener('message', function (e) {
const {port} = e.data;
port.onmessage = function (e) { // (A)
console.log(e.data);
};
});
});
In line A, we could also have used port.addEventListener('message', ···)
. But then you need to explicitly call port.start()
before you can receive messages. Unfortunately, the setter is more magical here than the method.
The nice thing about receiving messages is that the channel buffers posted messages. Therefore, there is no need to worry about race conditions (sending too early or listening too late).
Inlining Web Workers
The following tool function is used for inlining Web Workers.
function createWorker(workerFunc) {
if (! (workerFunc instanceof Function)) {
throw new Error('Argument must be function');
}
const src = `(${workerFunc})();`;
const blob = new Blob([src], {type: 'application/javascript'});
const url = URL.createObjectURL(blob);
return new Worker(url);
}
For older browsers, using separate source files is safer, because creating Workers from blobs can be buggy and/or unsupported.
Further reading
- “The Basics of Web Workers” by Eric Bidelman for HTML5 Rocks
- “
MessagePort
” on MDN
Acknowledgements: Tips on Twitter from @mourner, @nolanlawson and @Dolphin_Wood helped with this blog post.
Comments
Post a Comment