JavaScript: real parallelism with worker threads
Split an array between two worker threads and add their partial results.
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));
}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
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));}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
| Step / line | What happens | State after step | Output 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.