Пошук уроків, статей та іншого контенту
Виконуйте CPU-інтенсивні операції у Worker Threads, не блокуючи головний потік Node.js.
Node.js виконує JavaScript-код у головному потоці. Поки цей потік обробляє синхронну CPU-інтенсивну операцію, він не може реагувати на інші події:
нові HTTP-запити;
таймери;
повідомлення через сокети;
завершення операцій введення-виведення.
Worker Threads дають змогу запускати JavaScript-код в окремих потоках. Такий потік має власний цикл подій і власну пам’ять, але може обмінюватися повідомленнями з головним потоком.
Worker Threads корисні для:
складних математичних обчислень;
обробки великих масивів даних;
кодування або декодування;
хешування;
стиснення;
інших операцій, які активно використовують CPU.
Вони не потрібні для звичайних асинхронних операцій, наприклад читання файлу або запиту до бази даних. Node.js уже ефективно обробляє такі операції через асинхронні API.
worker_threadsВбудований модуль worker_threads надає основні інструменти для роботи з потоками:
Worker — створює новий потік;
isMainThread — показує, чи виконується код у головному потоці;
parentPortworkerData — початкові дані, передані під час створення worker-потоку.
Worker-потік не має спільного JavaScript-контексту з головним потоком. Змінна, створена в одному потоці, не стає автоматично доступною в іншому.
Для обміну даними використовуються методи:
worker.postMessage(value) — надсилає повідомлення worker-потоку;
parentPort.postMessage(value) — надсилає повідомлення головному потоку;
worker.on('message', callback) — отримує повідомлення від worker-потоку;
parentPort.on('message', callback) — отримує повідомлення від головного потоку.
Передані значення копіюються за допомогою механізму структурованого клонування. Тому зміна об’єкта в одному потоці не змінює його копію в іншому.
У наступному прикладі головний потік передає worker-потоку число 200000. Worker рахує кількість простих чисел у діапазоні від 2 до цього числа та повертає результат.
const {
Worker,
isMainThread,
parentPort,
workerData
} = require('node:worker_threads');
function isPrime(number) {
if (number < 2) {
return false;
}
if (number === 2) {
return true;
}
if (number % 2 === 0) {
return false;
}
for (let divisor = 3; divisor * divisor <= number; divisor += 2) {
if (number % divisor === 0) {
return false;
}
}
return true;
}
function countPrimes(limit) {
let count = 0;
for (let number = 2; number <= limit; number += 1) {
if (isPrime(number)) {
count += 1;
}
}
return count;
}
if (isMainThread) {
const limit = 200_000;
const worker = new Worker(__filename, {
workerData: { limit }
});
worker.on('message', (result) => {
console.log(`Кількість простих чисел до ${limit}: ${result}`);
});
worker.on('error', (error) => {
console.error('Помилка worker-потоку:', error);
});
worker.on('exit', (code) => {
if (code !== 0) {
console.error(`Worker завершився з кодом ${code}`);
}
});
console.log('Головний потік продовжує працювати...');
} else {
const { limit } = workerData;
const result = countPrimes(limit);
parentPort.postMessage(result);
}Запустіть файл командою:
node worker-threads.jsПослідовність роботи програми:
Node.js запускає файл у головному потоці.
isMainThread має значення true.
Створюється новий Worker, який повторно запускає цей самий файл.
У worker-потоці isMainThread має значення false.
Worker отримує limit через workerData.
Обчислення виконуються в окремому потоці.
Результат надсилається через parentPort.postMessage().
Головний потік отримує результат у слухачі події message.
workerDataПараметр workerData зручно використовувати для даних, які потрібні worker-потоку відразу після запуску:
const worker = new Worker('./worker.js', {
workerData: {
numbers: [10, 20, 30]
}
});У файлі worker-потоку ці дані доступні через workerData:
const { workerData, parentPort } = require('node:worker_threads');
const sum = workerData.numbers.reduce((total, number) => {
return total + number;
}, 0);
parentPort.postMessage(sum);workerData підходить для початкової конфігурації або одного набору вхідних даних. Якщо worker має обробляти багато завдань протягом свого життя, краще створити його один раз і надсилати завдання через postMessage().
Worker може залишатися активним і обробляти повідомлення одне за одним.
const { Worker } = require('node:worker_threads');
const worker = new Worker('./worker.js');
worker.on('message', (message) => {
console.log(`Результат для ${message.number}: ${message.result}`);
});
worker.on('error', (error) => {
console.error('Помилка:', error);
});
worker.postMessage({ number: 10 });
worker.postMessage({ number: 20 });
worker.postMessage({ number: 30 });
setTimeout(() => {
worker.terminate();
}, 100);worker.jsconst { parentPort } = require('node:worker_threads');
function square(number) {
return number * number;
}
parentPort.on('message', ({ number }) => {
const result = square(number);
parentPort.postMessage({
number,
result
});
});У реальному застосунку worker зазвичай не завершується після першого завдання. Він очікує наступні повідомлення, а завершення виконується явно за допомогою worker.terminate() або через завершення процесу.
Worker може завершитися через помилку. Для контролю його стану використовують події:
worker.on('message', (value) => {
// Обробка успішного результату
});
worker.on('error', (error) => {
// Обробка необробленої помилки worker-потоку
});
worker.on('exit', (code) => {
// Перевірка коду завершення
});Подія error повідомляє про помилку в worker-потоці. Після такої помилки worker зазвичай завершується.
Метод terminate() примусово зупиняє worker:
const exitCode = await worker.terminate();
console.log(`Worker завершено з кодом ${exitCode}`);Це корисно, якщо завдання скасували або worker більше не потрібен.
Worker Threads варто використовувати, якщо одночасно виконуються такі умови:
Операція є синхронною та CPU-інтенсивною.
Її виконання може зайняти помітний час.
Блокування головного потоку неприпустиме.
Вартість запуску worker-потоку виправдана.
Наприклад, довгий цикл обчислень у головному потоці може затримати обробку всіх інших подій. Перенесення цього циклу до worker-потоку залишає головний потік вільним.
Однак створення worker-потоку має накладні витрати. Для дуже маленьких операцій запуск окремого потоку може бути повільнішим, ніж виконання операції безпосередньо.
Якщо важке обчислення виконується до створення worker-потоку або всередині обробника головного потоку, воно все одно блокує Node.js.
Потрібно переносити саме CPU-інтенсивну частину в worker, а не лише викликати worker поруч із нею.
Створення потоку має певну вартість. Якщо завдання короткі, краще використовувати один довгоживучий worker і надсилати йому кілька повідомлень.
Для великої кількості завдань зазвичай створюють обмежену групу worker-потоків, а не окремий потік для кожного завдання.
Worker-потоки не ділять звичайні змінні:
let result = 0;Окремий worker не може напряму змінити result у головному потоці. Результат потрібно повернути повідомленням через postMessage().
error та exitБез обробки помилок може бути складно зрозуміти, чому обчислення не завершилося. Для worker-потоків, які виконують важливі завдання, потрібно обробляти щонайменше message, error і exit.
Передавання звичайних об’єктів і масивів може вимагати їх копіювання. Для великих обсягів даних це збільшує використання пам’яті та час обміну.
У випадках, де потрібне ефективне передавання бінарних даних, Node.js підтримує спеціальні механізми передачі ArrayBuffer, але їх слід застосовувати лише тоді, коли звичайного копіювання недостатньо.
Worker Threads запускають JavaScript-код в окремих потоках.
Вони допомагають виконувати CPU-інтенсивні операції без блокування головного потоку Node.js.
Worker створює потік, а isMainThread дає змогу відрізнити головний потік від worker-потоку.
workerData передає початкові дані під час створення worker.
postMessage() і parentPort використовуються для обміну повідомленнями.
Для контролю worker-потоку потрібно обробляти події message, error та exit.
Worker Threads не потрібні для звичайних асинхронних операцій введення-виведення.
Для багатьох коротких завдань ефективніше повторно використовувати worker, а не створювати новий для кожного завдання.