Worker threads в Node.js: параллельные вычисления
Содержание
О проверке кода. Примеры не запускались в этой сессии (по договорённости — фокус на контенте, а не прогонах). Говорим прямо, а не ставим бейдж «проверено».
Node.js знаменит однопоточностью, и обычно это плюс. Но у неё есть цена: одно тяжёлое вычисление способно заморозить весь сервер. Worker threads — ответ на эту проблему. Разберём, когда они нужны, а когда только вредят.
Проблема: блокировка основного потока
// ❌ тяжёлое вычисление в основном потоке
app.get('/report', (req, res) => {
const result = heavyComputation(); // считается 5 секунд
res.json(result);
});
// пока это считается, СЕРВЕР НЕ ОТВЕЧАЕТ никому — event loop заблокирован
Event loop Node крутится в одном потоке. Пока в нём выполняется синхронное тяжёлое вычисление (сложная математика, обработка большого массива), он не может обрабатывать другие запросы — сервер замирает для всех. Асинхронность здесь не спасает: async помогает с ожиданием (ввод-вывод), но не с загрузкой процессора. Вот тут и нужны worker threads.
Решение: вынести вычисление в поток
const { Worker } = require('node:worker_threads');
function runHeavyTask(data) {
return new Promise((resolve, reject) => {
const worker = new Worker('./heavy-worker.js', { workerData: data });
worker.on('message', resolve); // результат из потока
worker.on('error', reject);
});
}
// теперь основной поток свободен, пока worker считает
app.get('/report', async (req, res) => {
const result = await runHeavyTask(req.query);
res.json(result); // сервер всё это время отвечал другим!
});
// heavy-worker.js — код, выполняемый в отдельном потоке
const { workerData, parentPort } = require('node:worker_threads');
const result = heavyComputation(workerData); // считаем параллельно
parentPort.postMessage(result); // отдаём результат назад
Worker выполняет тяжёлую задачу в отдельном потоке, не блокируя основной. Пока поток считает, event loop продолжает обрабатывать другие запросы. Результат возвращается сообщением, которое удобно обернуть в промис. Сервер остаётся отзывчивым.
Обмен данными: сообщения, не общая память
// основной поток
worker.postMessage({ task: 'resize', file: 'photo.jpg' });
worker.on('message', (result) => console.log('готово:', result));
// в worker
parentPort.on('message', (data) => {
const result = process(data);
parentPort.postMessage(result);
});
Потоки обмениваются данными через сообщения, а не общую память. postMessage отправляет, обработчик message принимает. Важная деталь: данные копируются, а не разделяются между потоками — это защищает от гонок (race conditions), знакомых по многопоточности в других языках. Обратная сторона: копирование больших объёмов данных стоит ресурсов. Для действительно общей памяти есть SharedArrayBuffer, но он нужен редко.
Worker threads против cluster
Их постоянно путают — это разные инструменты для разных задач:
Worker threads
Потоки для вычислений
- Потоки внутри ОДНОГО процесса
- Для CPU-задач: расчёты, картинки
- Общая память возможна (SharedArrayBuffer)
- Ускоряют тяжёлую задачу
Cluster
Процессы для масштаба
- Несколько ПРОЦЕССОВ приложения
- Распределяют запросы по ядрам CPU
- Память раздельная, общего нет
- Масштабируют сервер под нагрузку
- cluster запускает несколько процессов — копий приложения — и распределяет входящие запросы между ними, задействуя все ядра. Это про масштаб сервера;
- worker threads — потоки внутри одного процесса для параллельных вычислений. Это про ускорение конкретной тяжёлой задачи.
Грубо: cluster — «больше воркеров, чтобы обслужить больше клиентов»; threads — «вынести тяжёлую математику, чтобы не морозить основной поток». Часто их даже сочетают.
Когда НЕ нужны worker threads
Это важнее, чем кажется — их применяют не по делу:
// ❌ БЕССМЫСЛЕННО — запрос к БД это ввод-вывод, не нагрузка на CPU
const worker = new Worker('./db-query.js'); // накладные расходы зря
Для операций ввода-вывода worker threads не нужны и вредны. Запрос к базе, чтение файла, HTTP-запрос — это ожидание, а не нагрузка на процессор; event loop справляется с ними асинхронно и без потоков. Создание worker и копирование данных стоят ресурсов — на мелких задачах эти накладные расходы больше выгоды. Worker threads оправданы только для CPU-задач:
- обработка изображений и видео;
- шифрование и хеширование больших объёмов;
- сложные математические расчёты, симуляции;
- парсинг и обработка больших наборов данных в памяти.
Правило: worker threads — для процессора, асинхронность — для ожидания. Перепутать — значит усложнить код без выгоды или даже замедлить его.
Смежные темы: event loop — почему блокировка опасна; cluster — масштабирование процессами; промисы — асинхронность для ввода-вывода; crypto — пример CPU-задачи. Полный список — в уроках Node.js.