Concurrency and Multithreading in JavaScript: Advanced Level
JavaScript has historically been a single-threaded language operating within the Event Loop. However, modern Node.js applications require high performance when handling CPU-intensive tasks. In this article, we will explore advanced concurrency and multithreading mechanisms: Worker Threads, SharedArrayBuffer, Atomics, and clustering. You will learn how to efficiently use multi-core processors and avoid blocking the main thread.
1. Worker Threads: True Multithreading in Node.js
The worker_threads module appeared in Node.js 10.5.0 and allows creating separate threads with their own V8 engine. Unlike clustering (processes), Worker Threads share memory and can exchange data via SharedArrayBuffer.
Creating and Running a Worker
// main.jsconst { Worker } = require('worker_threads');
function runWorker(data) { return new Promise((resolve, reject) => { const worker = new Worker('./worker.js', { workerData: data }); worker.on('message', resolve); worker.on('error', reject); worker.on('exit', (code) => { if (code !== 0) reject(new Error(`Worker stopped with exit code ${code}`)); }); });}
(async () => { const result = await runWorker({ numbers: [1, 2, 3, 4, 5] }); console.log('Result:', result); // { sum: 15 }})();// worker.jsconst { parentPort, workerData } = require('worker_threads');
const sum = workerData.numbers.reduce((acc, val) => acc + val, 0);parentPort.postMessage({ sum });Important: Workers do not have access to the DOM or modules requiring the main thread. Use them for heavy computations, image processing, or encryption.
2. SharedArrayBuffer and Atomics: Shared Memory
SharedArrayBuffer allows multiple workers to read and write data to the same memory area. The Atomics object is used for synchronization access — it guarantees atomic operations.
Example: Parallel Array Processing
// main.jsconst { Worker } = require('worker_threads');const { SharedArrayBuffer } = require('worker_threads');
const buffer = new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT * 10);const array = new Int32Array(buffer);
// Fill the arrayfor (let i = 0; i < 10; i++) array[i] = i;
const worker1 = new Worker('./worker_atomics.js', { workerData: { start: 0, end: 5 } });const worker2 = new Worker('./worker_atomics.js', { workerData: { start: 5, end: 10 } });
worker1.on('message', () => console.log('Worker 1 finished'));worker2.on('message', () => console.log('Worker 2 finished'));// worker_atomics.jsconst { parentPort, workerData } = require('worker_threads');const { Atomics } = require('worker_threads');
const buffer = new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT * 10);const array = new Int32Array(buffer);
// Atomic increment of each elementfor (let i = workerData.start; i < workerData.end; i++) { Atomics.add(array, i, 10);}
parentPort.postMessage('done');Note: Use Atomics.wait() and Atomics.notify() for thread synchronization (analogous to mutexes). This is critical for preventing race conditions.
3. Clustering: Scaling via Processes
The cluster module creates multiple Node.js processes (workers), each handling requests. This is the standard way to scale HTTP servers on multi-core systems.
Example: Clustered HTTP Server
const cluster = require('cluster');const http = require('http');const numCPUs = require('os').cpus().length;
if (cluster.isMaster) { console.