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 против 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.

Частые вопросы

Зачем нужны worker threads, если Node однопоточный?
Основной поток Node один, и тяжёлое вычисление в нём блокирует весь сервер. Worker threads выносят такие задачи в отдельные потоки, где они считаются параллельно, не мешая основному потоку обрабатывать запросы.
Чем worker threads отличаются от cluster?
Cluster запускает несколько процессов, каждый со своей копией приложения, для распределения запросов между ядрами. Worker threads — потоки внутри одного процесса для параллельных вычислений. Cluster масштабирует сервер, threads ускоряют тяжёлую задачу.
Когда стоит использовать worker threads?
Только для задач, нагружающих процессор: обработка изображений, шифрование, сложные вычисления, парсинг больших данных. Для операций ввода-вывода вроде запросов к базе они не нужны — там достаточно асинхронности event loop.
Как обмениваются данными основной поток и worker?
Через сообщения: postMessage отправляет данные, а обработчик on message их принимает. Данные копируются, а не разделяются, кроме SharedArrayBuffer. Это защищает от гонок, но большие объёмы дороже передавать.
Ускорят ли worker threads любой код?
Нет. Они помогают только процессорным задачам и вредят при мелких — создание потока и передача данных стоят ресурсов. Для ввода-вывода и лёгких операций обычная асинхронность быстрее, чем накладные расходы на потоки.