Что такое Worker Threads в Node.js и для чего они нужны?

Ответ

Worker Threads — это механизм в Node.js для выполнения JavaScript-кода в параллельных потоках. Они предназначены для выгрузки CPU-интенсивных (блокирующих) задач из основного потока Event Loop, чтобы не блокировать обработку входящих запросов и асинхронных операций I/O.

Ключевые отличия от кластеризации (cluster):

  • Кластер (cluster): Создаёт несколько независимых процессов (инстансов Node.js), каждый со своей памятью. Подходит для масштабирования сетевых приложений, так как процессы не разделяют память и общаются через IPC.
  • Worker Threads: Создаёт несколько потоков в рамках одного процесса. Потоки могут (опционально) разделять память через SharedArrayBuffer, что делает их более подходящими для параллельных вычислений над общими данными.

Основные концепции и API:

  • Worker: Класс для создания нового потока.
  • parentPort: Канал для обмена сообщениями между главным потоком и воркером.
  • workerData: Позволяет передать начальные данные в воркер при его создании.
  • MessageChannel: Создаёт канал для прямого общения между воркерами.

Пример: вычисление числа Фибоначчи в отдельном потоке

main.js (главный поток):

const { Worker } = require('worker_threads');
const path = require('path');

function runFibonacciService(n) {
    return new Promise((resolve, reject) => {
        // Создаём воркер и передаём ему данные
        const worker = new Worker(path.join(__dirname, 'fibonacci-worker.js'), {
            workerData: n
        });

        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 () => {
    console.log('Main thread: starting heavy calculation...');
    const result = await runFibonacciService(40); // Блокирующая операция
    console.log(`Main thread: result = ${result}`);
    // Event Loop в основном потоке не был заблокирован!
})();

fibonacci-worker.js (код воркера):

const { parentPort, workerData } = require('worker_threads');

// Рекурсивная функция, тяжелая для CPU
function fibonacci(n) {
    if (n <= 1) return n;
    return fibonacci(n - 1) + fibonacci(n - 2);
}

// Выполняем вычисление и отправляем результат обратно в главный поток
const result = fibonacci(workerData);
parentPort.postMessage(result);

Когда использовать Worker Threads:

  • Обработка изображений/видео.
  • Синтаксический анализ или сжатие больших объёмов данных.
  • Сложные математические вычисления, машинное обучение.
  • Любые синхронные задачи, которые занимают более 10-30 мс и блокируют Event Loop.

Важно: Worker Threads не предназначены для увеличения производительности I/O-операций (работа с файлами, сетью). Для этого эффективнее использовать асинхронные API самого Node.js.

Ответ 18+ 🔞

Давай разберемся с этими Worker Threads, а то мозг уже плавится, как сырок в микроволновке. Представь себе, что твой основной поток Event Loop — это одинокий официант в дохуя популярной забегаловке. Клиенты (запросы) лезут со всех сторон, а он один, и если какому-то гостю взбредет в голову заказать не бургер, а, например, вручную посчитать число Пи до миллиардного знака, то все остальные просто сдохнут с голоду, пока он там цифры складывает. Вот это и есть блокирующая CPU-задача. Пиздец, а не обслуживание.

Так вот, Worker Threads — это нанятые на подхват дополнительные официанты (потоки), которые работают внутри того же самого ресторана (процесса). Ты этого сумасшедшего математика отправляешь на кухню к отдельному повару-воркеру, а твой главный поток продолжает разносить салатики и принимать новые заказы. И все довольны.

Чем это не является, ёпта? Не путай это с кластеризацией (cluster). Это как будто ты открыл не кухню в том же ресторане, а целую сеть филиалов (отдельные процессы). У каждого свой зал, своя кухня, своя касса. Общаются они между собой только курьерами (IPC). Это круто для масштабирования, когда у тебя тупо много клиентов, но если тебе нужно быстро перетереть одну тонну картошки на всех — не очень удобно, картошку по филиалам возить.

А Worker Threads — это когда у тебя в одном большом ресторане несколько кухонь, и повара могут, если надо, делить одну гигантскую тушу мяса (SharedArrayBuffer). Для параллельных вычислений — то, что доктор прописал.

Из чего это всё собрано, эта конструкция:

  • Worker: Собственно, сам новый поток. Нанимаешь сотрудника.
  • parentPort: Та самая трубка на кухню, чтобы официант мог крикнуть "Заказ готов!" или "Повар, ты чего там, обосрался что ли?".
  • workerData: Ты можешь сразу при найме воркера сунуть ему в карман записку с заданием. "Вот тебе число 40, иди посчитай Фибоначчи, не отвлекайся".
  • MessageChannel: Это если твои два повара-воркера хотят пообщаться между собой напрямую, без главного официанта. Типа "Дай сольку, братан".

Пример: как отправить считать Фибоначчи, чтобы не ебать основной поток

main.js (главный поток, он же забегаловка):

const { Worker } = require('worker_threads');
const path = require('path');

function runFibonacciService(n) {
    return new Promise((resolve, reject) => {
        // Наняли нового повара, дали ему инструкцию (файл worker-а)
        const worker = new Worker(path.join(__dirname, 'fibonacci-worker.js'), {
            workerData: n // И сунули в карман бумажку "n=40"
        });

        worker.on('message', resolve); // Услышали с кухни "Готово!"
        worker.on('error', reject);    // Услышали "Ой, всё! Кастрюля взорвалась!"
        worker.on('exit', (code) => {
            if (code !== 0) {
                reject(new Error(`Повар сбежал с криком ${code}, всё пропало!`));
            }
        });
    });
}

// Использование
(async () => {
    console.log('Главный поток: Ща тут тяжёлую хуйню будем считать...');
    const result = await runFibonacciService(40); // Отправили на кухню
    console.log(`Главный поток: Всё, принесли! Результат = ${result}`);
    // И главное — Event Loop тут не лёг костьми, он продолжал работать!
})();

fibonacci-worker.js (повар на кухне-воркере):

const { parentPort, workerData } = require('worker_threads');

// Сама рекурсивная функция, которая жрёт процессор как не в себя
function fibonacci(n) {
    if (n <= 1) return n;
    return fibonacci(n - 1) + fibonacci(n - 2);
}

// Достаём из кармана задание, пашем, и кричим в трубку результат
const result = fibonacci(workerData);
parentPort.postMessage(result);

Когда это вот всё нужно применять на практике?

  • Обработка изображений/видео. Когда нужно скормить фото нейросети, а она будет думать овердохуища.
  • Сжатие или парсинг гигабайтов логов. Чтоб не зависнуть на полчаса.
  • Сложная математика, машинное обучение. Всякие матрицы, графы, хуи-моржовые.
  • Любые синхронные задачи, которые тупо блокируют Event Loop больше чем на 10-30 мс. Если твоя операция может заставить всех клиентов ждать — это верный знак, что её надо выпихивать в воркер.

Важный момент, блядь, запомни: Worker Threads — это НЕ волшебная таблетка для I/O. Не для чтения файлов, не для сетевых запросов. Для этого в Node.js и так есть асинхронные API, которые справляются на ура. Воркеры нужны именно когда процессор упирается лбом в стенку и начинает тупить. Всё, теперь ты понял, да? Иди делай.