Concurrency and Multithreading in JavaScript: Advanced Level
\n\nJavaScript is traditionally considered a single-threaded language, but the modern Node.js ecosystem provides powerful tools for working with concurrency and true multithreading. In this article, we will explore advanced techniques: from the intricacies of the Event Loop to using Worker Threads, SharedArrayBuffer, and Atomics. You will learn how to efficiently distribute workloads, avoid race conditions, and build scalable applications.
\n\n1. Event Loop, Microtasks and Macrotasks: The Foundation of Concurrency
\n\nUnderstanding the Event Loop is key to managing concurrency without blocking. In Node.js, the event loop consists of several phases: timers, I/O callbacks, idle, poll, check (setImmediate), and close. It is important to distinguish between microtasks (Promise, process.nextTick) and macrotasks (setTimeout, setInterval, I/O).
\n\nMicrotasks are executed immediately after the current phase completes, before the next macrotask. This can lead to Event Loop starvation if microtasks are generated indefinitely.
\n\n// Example: microtasks block macrotasks\nsetTimeout(() => console.log('Macrotask'), 0);\n\nPromise.resolve().then(() => {\n // Infinite microtask\n process.nextTick(() => {\n console.log('Microtask');\n // Recursive call will block the Event Loop\n process.nextTick(() => process.nextTick(() => {}));\n });\n});\n// Output: 'Microtask' (infinite), 'Macrotask' never executes\n\nFor advanced control, use queueMicrotask() and setImmediate(). Remember: process.nextTick has priority over Promise microtasks.
2. Worker Threads: True Multithreading in Node.js
\n\nThe worker_threads module allows you to execute JavaScript in parallel threads. Each Worker has its own V8 instance and isolated memory. Data exchange happens via messages or SharedArrayBuffer.
Creating a simple Worker:
\n\n// main.js\nconst { Worker } = require('worker_threads');\n\nfunction runWorker(file, data) {\n return new Promise((resolve, reject) => {\n const worker = new Worker(file, { workerData: data });\n worker.on('message', resolve);\n worker.on('error', reject);\n worker.on('exit', (code) => {\n if (code !== 0) reject(new Error(`Worker stopped with code ${code}`));\n });\n });\n}\n\n// Run\nrunWorker('./worker.js', { iterations: 1e9 })\n .then(result => console.log('Result:', result))\n .catch(err => console.error(err));\n\n// worker.js\nconst { parentPort, workerData } = require('worker_threads');\n\nfunction heavyComputation(n) {\n let sum = 0;\n for (let i = 0; i < n; i++) { sum += Math.sqrt(i); }\n return sum;\n}\n\nparentPort.postMessage(heavyComputation(workerData.iterations));\n\nImportant: A Worker does not have access to the DOM, global process objects (process.env), or modules loaded in the main thread. Only pass serializable data.
\n\nWorker Pool
\nCreating a Worker for each task is expensive. Use a pool:
\n\nclass WorkerPool {\n constructor(file, size = 4) {\n this.workers = [];\n this.queue = [];\n for (let i = 0; i < size; i++) {\n const worker = new Worker(file);\n worker.on('message', (result) => this._handleResult(worker, result));\n worker.on('error', (err) => this._handleError(worker, err));\n this.workers.push(worker);\n }\n }\n\n runTask(data) {\n return new Promise((resolve, reject) => {\n this.queue.push({ data, resolve, reject });\n this._processQueue();\n });\n }\n\n _processQueue() {\n if (this.queue.length === 0) return;\n const worker = this.workers.find(w => !w.busy);\n if (!worker) return;\n worker.busy = true;\n const task = this.queue.shift();