Ответ
Хотя Node.js однопоточен, для CPU-интенсивных задач я использовал модуль worker_threads и библиотеки уровня userland. Вот практический пример из проекта по обработке изображений.
1. Создание пула воркеров с библиотекой piscina:
npm install piscina
// worker-script.js - файл, который будет выполняться в потоке
const { parentPort } = require('worker_threads');
const sharp = require('sharp'); // CPU-heavy библиотека для изображений
parentPort.on('message', async (task) => {
const { imageBuffer, operations } = task;
try {
let processor = sharp(imageBuffer);
// Применяем операции (ресайз, обрезка, фильтры)
if (operations.resize) {
processor = processor.resize(operations.resize.width, operations.resize.height);
}
const processedBuffer = await processor.toBuffer();
parentPort.postMessage({ success: true, buffer: processedBuffer });
} catch (error) {
parentPort.postMessage({ success: false, error: error.message });
}
});
// main.js - основной процесс
const Piscina = require('piscina');
const path = require('path');
const fs = require('fs').promises;
// Создаём пул из 4 воркеров (по числу ядер CPU)
const pool = new Piscina({
filename: path.resolve(__dirname, 'worker-script.js'),
minThreads: 2,
maxThreads: 4
});
async processBatchOfImages(imagePaths) {
const tasks = imagePaths.map(async (imagePath) => {
const imageBuffer = await fs.readFile(imagePath);
// Отправляем задачу в пул воркеров
return pool.run({
imageBuffer,
operations: { resize: { width: 800, height: 600 } }
});
});
// Обрабатываем результаты по мере готовности
const results = await Promise.allSettled(tasks);
results.forEach((result, index) => {
if (result.status === 'fulfilled' && result.value.success) {
console.log(`Изображение ${imagePaths[index]} обработано`);
// Сохраняем result.value.buffer ...
} else {
console.error(`Ошибка обработки ${imagePaths[index]}:`, result.reason || result.value?.error);
}
});
}
2. Использование SharedArrayBuffer для обмена данными (осторожно!):
Для задач, где нужно минимизировать копирование данных между потоками (например, обработка большого бинарного файла), использовал SharedArrayBuffer.
// В основном потоке
const sharedBuffer = new SharedArrayBuffer(1024 * 1024); // 1MB
const arr = new Uint8Array(sharedBuffer);
// Заполняем массив данными...
// Передаём sharedBuffer в воркер
pool.run({ buffer: sharedBuffer }, { transferList: [sharedBuffer] });
3. Мониторинг и управление пулом:
Библиотеки уровня userland, такие как piscina, предоставляют статистику.
console.log(`Активных задач: ${pool.queueSize}`);
console.log(`Занято воркеров: ${pool.completed}`);
Ключевые выводы:
- Worker Threads идеальны для длительных синхронных вычислений (математика, обработка изображений/видео, шифрование).
- Не используйте их для I/O операций — для этого асинхронный Event Loop Node.js эффективнее.
- Пул воркеров (
piscina) предотвращает накладные расходы на создание потока для каждой задачи. - Обмен сообщениями между потоками имеет свою стоимость, поэтому лучше отправлять одну большую задачу, чем много мелких.
В своём проекте это позволило ускорить обработку пачки из 100 изображений с 30 секунд (последовательно) до примерно 8 секунд (4 потока).