Must know patterns in Web Workers

Must know patterns in Web Workers

I will write about 2 interesting patterns in web workers that I recently came across while reading through the book "Multithreaded Javascript: Concurrency beyond Event Loop" by Thomas Hunter || and Brian English. I highly encourage you to read this book as you will realize (just like I did) how even the advanced concepts are more straightforward and easier to understand.

Generally, we use Web Workers to offload some of the computationally heavy tasks from the main thread. This can be performing some complex calculations like Fibonacci, prime numbers, and running a loop of a really larger value for some task.

If we use a dedicated worker just for a single task, then it would be simpler like:

// from main thread
worker.postMessage(fibonacciNumber);
// it could be anything related to the task.        

And then in the worker file, we would do something like this:

// worker.js

self.onmessage = (data) => {
    // perform the task with the data value as input
    postMessage(output); // result to main thread.
}        

If we happened to use the same worker for multiple tasks, then we would have done something like this:

// from main thread
worker.postMessage('prime|start:2,end:100');
worker.postMessage('fibonacci|num:33');
// This essentially tells our worker to perform the task: fibonacci
// for the num: 33        

We would have a method in the worker that splits the strings and identifies the task and the inputs. The same is the case when we use JSON to map them instead of just strings.

NOTE: There is another problem here. When we pass 2 tasks to the worker at the same time, we need a way to identify what response maps to what request.

We can address these problems using a combination of the following patterns:

  1. The RPC (Remote Procedure Call) pattern
  2. The Command Dispatcher pattern

The RPC (Remote Procedure Call) pattern

Quoting from the book itself,

The RPC (Remote Procedure Call) pattern is a way to take a representation of a function and its arguments, serialize them, and pass them to a remote destination to have them get executed. The string square_sum | num:1000000 is actually a form of RPC that we just accidentally invented. Perhaps it could ultimately get converted into a function call like squareNum(1000000), but that will be considered next in “The Command Dispatcher Pattern”.

In order for us to implement this pattern, we can take the help of the existing JSON-RPC standard. This is easier to implement and this standard defines the JSON representation for the request and the response.

Transforming our existing implementation to use this standard, we get this:

// worker.postMessage

{"jsonrpc": "2.0", "method": "square_sum", "params": [4], "id": 1}

{"jsonrpc": "2.0", "method": "fibonacci", "params": [7], "id": 2}



// worker.onmessage

{"jsonrpc": "2.0", "result": "3524578", "id": 2}

{"jsonrpc": "2.0", "result": 4.1462643, "id": 1}        

With this, we can easily map the request with its response and also we have a standard way of passing the message back and forth.

While the RPC pattern is useful for defining protocols, it doesn’t necessarily provide a mechanism for determining what code path to execute on the receiving end

The Command Dispatcher pattern

The command dispatcher pattern is a way to take a serialized command, find the appropriate function, and then execute it, optionally passing in arguments.

This pattern is fairly straightforward to implement and doesn’t require a whole lot of magic. First, we can assume that there are two variables that contain relevant information about the method or “command” that the code needs to run. The first variable is called the method and is a string. The second variable is called args and is an array of values to be passed into the method. Assume these have been pulled from the RPC layer of the application.

We can make use of normal javascript objects to keep an index of the valid commands. Another important concept is that only defined commands should be executed. If the caller wants to invoke a method that doesn’t exist, an error should be gracefully generated that can be returned to the caller, without crashing the web worker.

const commands = {
  square_sum(max) {
    // ...
  },
  fibonacci(limit) {
    // ...
  }
};

function dispatch(method, args) {
  if (commands.hasOwnProperty(method)) { 
    return commands[method](...args); 
  }
  throw new TypeError(`Command ${method} not defined!`);
}        

Writing this inside the onmessage would look something like this:

self.onmessage = (rpc) => {
  const { method, params, id } = rpc;


  if (commands.hasOwnProperty(method)) {
    const result = await commands[method](...params);
    return { result, id }; 
  }


  return { 
    error: {
      code: -32601, // Any defined value/range
      message: `method ${method} not found`
    },
    id
  };
};        

If you are not already using these patterns, you can consider implementing them in order to make the flows cleaner and easier to read and modify.


Cheers

Arunkumar Sri Sailapathi.

Wonderful article with clear explanation. Great work Arunkumar Srisailapathi

Like
Reply

To view or add a comment, sign in

Others also viewed

Explore content categories