Using worker_threads in Node.js

This is a beginner’s guide to using worker_threads in Node.js. It does not assume you already understand Web Workers. (The API for worker_threads is based on Web Workers.) Let’s dive in.

This photo by Zachary Nelson is one that came up on Unsplash when I searched for “threads”.

Loading the module

worker_threads became available in Node.js 10.5.0. Prior to Node.js 11.7.0, you could not access the module unless you started node with the --experimental-worker flag.

$ node -e "require('worker_threads'); console.log('success');"
internal/modules/cjs/loader.js:605
throw err;
^
Error: Cannot find module 'worker_threads'
at Function.Module._resolveFilename (internal/modules/cjs/loader.js:603:15)
at Function.Module._load (internal/modules/cjs/loader.js:529:25)
at Module.require (internal/modules/cjs/loader.js:657:17)
at require (internal/modules/cjs/helpers.js:22:18)
at [eval]:1:1
at Script.runInThisContext (vm.js:123:20)
at Object.runInThisContext (vm.js:312:38)
at Object.<anonymous> ([eval]-wrapper:6:22)
at Module._compile (internal/modules/cjs/loader.js:721:30)
at evalScript (internal/bootstrap/node.js:720:27)
$
$
$ node --experimental-worker -e "require('worker_threads'); console.log('success');"
success
$

As of this writing, worker_threads are an experimental feature. Theoretically, that means breaking changes could happen at any time and you should not use worker_threads in production.

What are they good for?

As the documentation says:

Workers are useful for performing CPU-intensive JavaScript operations; do not use them for I/O, since Node.js’s built-in mechanisms for performing operations asynchronously already treat it more efficiently than Worker threads can.

worker_threads are more lightweight than the parallelism you can get usingchild_process or cluster. Additionally, worker_threads can share memory efficiently.

Hello world!

In the following example, Worker is the constructor for a worker. It requires an argument which is the path to a file containing the code for the worker to execute. In this case, we send it __filename so that the code that launches the worker and the code for the worker itself are in the same file. The constructor also takes an optional second options argument, but we do not use it here.

To differentiate whether we are in the main thread (which will launch the worker) or the worker itself, we use isMainThread. It is true if we are in the main thread (that is, not in a worker) and false if we are in a worker.

Once the worker is created, we listen (in the main thread) for the message event on the worker and use console.log() to print whatever is sent.

Lastly, to send a message from the worker to the main thread, we use parentPort.postMessage().

Put this code in a file called threads-example.js, invoke it with node and the --experimental-worker flag, and the output should be Hello world!.

$ node --experimental-worker threads-example.js 
Hello world!
$

Calculating Primes

Now let’s do something a little more interesting. Without worker_threads, here is how you might calculate all the prime numbers less than 10,000,000:

The code above was ported from a C# implementation at https://stackoverflow.com/a/34429272/436641. The important thing to know is that the generatePrimes() function does CPU-intensive work.

On my laptop, running the code above with the time utility reports results along the lines of:

real 0m17.209s
user 0m15.589s
sys 0m0.242s

Now let’s see what happens with worker_threads:

The new concept here is workerData. You set a value for the workerDataoption when invoking the Worker constructor. The value of workerData is cloned and available in the worker thread as require('worker_threads').workerData.

Running this script and passing it 2 on the command line (as the number of threads to use) yields far better performance than the single-threaded version:

real 0m7.881s
user 0m12.832s
sys 0m0.162s

Next Steps

Thanks, Anna Henningsen, for reviewing a draft of this post.