Worker Threads: параллелизм
worker_threads — встроенный модуль Node.js (с v12), позволяющий создавать настоящие параллельные потоки в одном процессе для выполнения CPU-интенсивных задач без блокировки Event Loop.
Зачем нужно
Node.js однопоточный: тяжёлые вычисления (криптография, парсинг CSV, ML-инференс, обработка изображений) блокируют Event Loop и делают сервер неотзывчивым. Worker Threads решают эту проблему: каждый воркер — отдельный V8-экземпляр в отдельном потоке, данные передаются через postMessage. В отличие от child_process.fork, воркеры разделяют память через SharedArrayBuffer.
Где используется
- Тяжёлые вычисления (парсинг больших файлов, SHA/bcrypt)
- Обработка изображений (resize, watermark)
- Статический анализ кода, линтинг
- ML-инференс (ONNX Runtime, TensorFlow.js)
- Параллельные задачи на CPU в server-side рендеринге
Основной контент
Базовый пример
// worker.js — код, выполняющийся в воркере
const { workerData, parentPort } = require('worker_threads');
function heavyComputation(n) {
let result = 0;
for (let i = 0; i < n; i++) result += Math.sqrt(i);
return result;
}
const result = heavyComputation(workerData.count);
parentPort.postMessage({ result });
// main.js — создание воркера
const { Worker } = require('worker_threads');
const path = require('path');
function runWorker(data) {
return new Promise((resolve, reject) => {
const worker = new Worker(path.join(__dirname, 'worker.js'), {
workerData: data
});
worker.on('message', resolve);
worker.on('error', reject);
worker.on('exit', (code) => {
if (code !== 0) reject(new Error(`Worker exited with code ${code}`));
});
});
}
// Не блокирует Event Loop
const { result } = await runWorker({ count: 1e9 });
console.log('Result:', result);
Worker Pool — пул воркеров
// workerPool.js — переиспользование воркеров вместо создания новых
const { Worker } = require('worker_threads');
const os = require('os');
class WorkerPool {
constructor(workerFile, size = os.cpus.length) {
this.workers = ;
this.queue = ;
for (let i = 0; i < size; i++) {
this._addWorker(workerFile);
}
}
_addWorker(file) {
const worker = new Worker(file);
worker.on('message', (result) => {
const { resolve } = worker._task;
worker._task = null;
this.workers.push(worker);
resolve(result);
this._processQueue;
});
this.workers.push(worker);
}
_processQueue {
if (this.queue.length > 0 && this.workers.length > 0) {
const { data, resolve, reject } = this.queue.shift();
const worker = this.workers.pop();
worker._task = { resolve, reject };
worker.postMessage(data);
}
}
run(data) {
return new Promise((resolve, reject) => {
this.queue.push({ data, resolve, reject });
this._processQueue;
});
}
}
module.exports = WorkerPool;
SharedArrayBuffer — разделяемая память
// Создать разделяемый буфер (без копирования данных)
const sharedBuffer = new SharedArrayBuffer(4 * Int32Array.BYTES_PER_ELEMENT);
const sharedArray = new Int32Array(sharedBuffer);
const worker = new Worker('./worker.js', {
workerData: { buffer: sharedBuffer }
});
// В worker.js:
// const { workerData } = require('worker_threads');
// const arr = new Int32Array(workerData.buffer);
// arr[0] = 42; // изменение видно в main.js
// Атомарные операции (thread-safe)
Atomics.add(sharedArray, 0, 1); // атомарный инкремент
Atomics.load(sharedArray, 0); // атомарное чтение
Inline Workers (без отдельного файла)
const { Worker, isMainThread, parentPort } = require('worker_threads');
if (isMainThread) {
const worker = new Worker(__filename); // передаём тот же файл
worker.on('message', console.log);
} else {
// Код воркера
parentPort.postMessage('Hello from worker!');
}
Частые ошибки
- Создавать воркер на каждый запрос — создание Worker дорого; использовать пул воркеров
- Передавать несериализуемые объекты —
postMessageиспользует structured clone; функции, классы-инстансы — нельзя передать - Использовать Worker для I/O задач — для файлов и сети Node.js и так неблокирующий; Worker нужен только для CPU
- Не обрабатывать 'exit' событие — без обработки падение воркера останется незамеченным
Связанные темы
Ресурсы
🎓 Источник: Atomics, SharedArrayBuffer, worker_threads в Node.js
- 📅 2019-02-21 · YouTube ·
zLm8pnbxSII - Тезисы:
- Worker очень похож на
child_process/cluster— те жеmessage,error,exit.Worker— наследник EventEmitter isMainThreadразличает мастер и воркер: можно запускать тот же файл черезnew Worker(__filename)- Передача данных: обычный объект сериализуется V8 (structured clone),
SharedArrayBufferпробрасывается ссылкой без копирования process.exit(code)внутри воркера НЕ завершает процесс — код попадает вworker.on('exit', ...)мастера- Воркер «висит» если у него есть подписки (event loop живой); без них — выходит сразу
worker.terminate— принудительное убийство, возвращает код выхода- Мастер ловит SIGTERM/Ctrl-C и сигналит воркерам выключиться, иначе процессы остаются
- Внутри воркера
parentPort— аналог EventEmitter, но сpostMessage/on('message')вместоemit/on
- Worker очень похож на
- Код:
const { Worker, isMainThread, parentPort, workerData } = require('worker_threads'); if (isMainThread) { const buffer = new SharedArrayBuffer(1024); const w = new Worker(__filename, { workerData: { buffer } }); w.on('message', console.log); } else { const arr = new Uint8Array(workerData.buffer); setInterval( => parentPort.postMessage(arr[0]++), 100); }
🎓 Источник: Потоки и процессы в JavaScript и Node.js — Опять однопоточный
- 📅 2026-02-23 · YouTube ·
pTnV6iNwAO0 - Тезисы:
- Различать CPU-bound («числодробилки», много циклов — блокируют) и I/O-bound (Node и так неблокирующий)
- В Node два типа потоков: libuv worker pool (для I/O, не управляете) и userland worker_threads (создаёте сами для CPU)
- Создание воркера дорого — нужен task runner / worker pool поверх; в Metarhia есть
noeu-runtimeи пул внутри Impress - В правильном раннере вместо ручного создания пишешь обычный
await application.invoke(method, args, exclusive)— низкоуровневая машинерия прячется exclusive: true— монопольный захват потока, обязательно для числодробилок- Между V8-рантаймами данные сериализуются (через MessagePort) — это всё равно быстрее межпроцессного обмена (Redis/Bull: данные «лазят 4 раза»)
SharedArrayBufferисключает копирование вообще — но требует mutex/semaphore/Atomics- Worker threads — официально стабильны с Node 12, по меркам экосистемы это «сравнительно недавно»
- Цитата:
«Не хотелось бы ходить на отдельную машину, передавать туда эти запросы. <...> Тут threads — она вообще не покидает рамок одного процесса операционной системы.»