JavaScript: real parallelism with worker threads

Split an array between two worker threads and add their partial results.

JavaScript
const { Worker, isMainThread, parentPort, workerData } = require("node:worker_threads");

if (isMainThread) {
  const data = Array.from({ length: 1000 }, (_, i) => i + 1);
  const halves = [data.slice(0, 500), data.slice(500)];
  const jobs = halves.map((part) => new Promise((resolve, reject) => {
    const worker = new Worker(__filename, { workerData: part });
    worker.on("message", resolve);
    worker.on("error", reject);
  }));
  Promise.all(jobs).then((sums) => {
    console.log(sums.join(" ") + " total " + sums.reduce((a, b) => a + b, 0));
  });
} else {
  parentPort.postMessage(workerData.reduce((a, b) => a + b, 0));
}
Output
125250 375250 total 500500

Explanation

To use more than one core, Node.js provides worker_threads. Each Worker runs its own JavaScript engine with its own memory, so workers do not share ordinary variables. They communicate by messages: data passed in workerData or sent with postMessage is copied (or transferred).

This program is both parent and worker. isMainThread tells which role the current copy has. The main thread starts two workers on the same file, each gets half the numbers, and each posts back its sum. Because nothing is shared, there are no data races. The messages are the synchronization.

Explore the dry run
Guided walkthroughReady
1const { Worker, isMainThread, parentPort, workerData } = require("node:worker_threads");
2
3if (isMainThread) {
4 const data = Array.from({ length: 1000 }, (_, i) => i + 1);
5 const halves = [data.slice(0, 500), data.slice(500)];
6 const jobs = halves.map((part) => new Promise((resolve, reject) => {
7 const worker = new Worker(__filename, { workerData: part });
8 worker.on("message", resolve);
9 worker.on("error", reject);
10 }));
11 Promise.all(jobs).then((sums) => {
12 console.log(sums.join(" ") + " total " + sums.reduce((a, b) => a + b, 0));
13 });
14} else {
15 parentPort.postMessage(workerData.reduce((a, b) => a + b, 0));
16}

Follow the example

Start to inspect the source, values, and output. These prepared steps explain the published sample; grouped loop steps describe multiple iterations.

View the complete dry-run table
Prepared trace of the published program. Some rows group repeated iterations.
Step / lineWhat happensState after stepOutput so far
The same file runs twice: once as the main thread and once in each worker.—
—
Start a worker thread and copy its half of the data to it.2 workers running
—
In the worker: sum the numbers received and post the result back.2 workers running
—
Combine the two partial sums: 125250 + 375250.total = 500500
125250 375250 total 500500

Cost and common mistakes

O(n) work in total; ideal elapsed time O(n / p) on p cores plus start-up cost.

  • Starting a worker costs time and memory. For small jobs it is slower than doing the work on the main thread.
  • Copying large data to a worker is expensive. SharedArrayBuffer and transferable ArrayBuffers avoid copies.
  • A worker that throws sends an error event. Handle it or the failure is lost.

Try it yourself

How would you use four workers?

Reveal answer and reasoning

Split the array into four slices, start four workers, and wait for the four messages with Promise.all.

Further reading