Пошук уроків, статей та іншого контенту
Передавайте дані між worker-потоками й процесами та обирайте відповідний механізм комунікації.
Потоки й процеси мають окремі контексти виконання. Змінна, створена в одному контексті, не стає автоматично доступною в іншому.
Для передавання даних Node.js використовує різні механізми:
worker-потоки:
worker.postMessage() і parentPort.postMessage();
MessageChannel для окремих каналів;
SharedArrayBuffer, якщо потрібна спільна пам’ять;
дочірні процеси:
IPC-канал через child_process.fork();
child.send() і обробник події 'message'.
У більшості випадків повідомлення передаються копіюванням. Тому зміна об’єкта в отримувача не змінює оригінал у відправника.
Модуль node:worker_threads дає змогу запускати JavaScript у додатковому потоці.
Головний потік і worker обмінюються повідомленнями через:
worker.postMessage(value) — повідомлення з головного потоку до worker;
parentPort.postMessage(value)
подію 'message' — отримання повідомлення.
Дані передаються за правилами структурованого клонування. Зазвичай можна передавати:
числа, рядки, boolean і null;
масиви та звичайні об’єкти;
Map, Set, Date;
типізовані масиви та буфери.
Функції та об’єкти з нестандартним станом не можна безпосередньо передати як звичайне повідомлення.
У цьому прикладі один файл працює у двох режимах: як головний потік і як worker.
// worker-message.js
const { Worker, isMainThread, parentPort } = require('node:worker_threads');
if (isMainThread) {
const worker = new Worker(__filename);
worker.on('message', (message) => {
console.log('Головний потік отримав:', message);
});
worker.on('error', (error) => {
console.error('Помилка worker:', error);
});
worker.on('exit', (code) => {
console.log(`Worker завершився з кодом ${code}`);
});
worker.postMessage({
operation: 'sum',
values: [4, 7, 9]
});
} else {
parentPort.on('message', (message) => {
if (message.operation !== 'sum') {
parentPort.postMessage({
error: 'Невідома операція'
});
return;
}
const result = message.values.reduce((sum, value) => sum + value, 0);
parentPort.postMessage({
operation: message.operation,
result
});
});
}Запуск:
node worker-message.jsРезультат буде подібним до такого:
Головний потік отримав: { operation: 'sum', result: 20 }
Worker завершився з кодом 0Канал є двостороннім: worker може отримати кілька повідомлень і відповісти на кожне з них.
// worker-request.js
const { Worker, isMainThread, parentPort } = require('node:worker_threads');
if (isMainThread) {
const worker = new Worker(__filename);
let responses = 0;
worker.on('message', (message) => {
console.log('Відповідь:', message);
responses += 1;
if (responses === 2) {
worker.terminate();
}
});
worker.postMessage({ id: 1, value: 10 });
worker.postMessage({ id: 2, value: 25 });
} else {
parentPort.on('message', ({ id, value }) => {
parentPort.postMessage({
id,
value,
doubled: value * 2
});
});
}Поле id допомагає зіставити відповідь із конкретним запитом, якщо повідомлення обробляються асинхронно або відповіді можуть надходити в іншому порядку.
Звичайне повідомлення копіює дані. Для великих бінарних буферів це може бути зайвою витратою пам’яті та часу.
ArrayBuffer можна передати через список передавання — transferList. У такому випадку право володіння буфером переходить до отримувача.
// transfer-buffer.js
const { Worker, isMainThread, parentPort } = require('node:worker_threads');
if (isMainThread) {
const worker = new Worker(__filename);
const buffer = new ArrayBuffer(4);
const input = new Uint8Array(buffer);
input.set([10, 20, 30, 40]);
worker.on('message', (message) => {
console.log('Буфер після обробки:', message);
console.log('Розмір початкового буфера:', buffer.byteLength);
worker.terminate();
});
// Буфер передається без копіювання.
worker.postMessage(buffer, [buffer]);
} else {
parentPort.on('message', (buffer) => {
const values = new Uint8Array(buffer);
for (let index = 0; index < values.length; index += 1) {
values[index] *= 2;
}
parentPort.postMessage(Array.from(values));
});
}Після передавання ArrayBuffer його розмір у відправника стає 0: буфер більше не належить головному потоку. Тому не слід використовувати початковий буфер після передавання.
Список передавання використовують, коли:
дані великі;
отримувач може стати єдиним власником даних;
копіювання було б дорогим.
Для звичайних невеликих об’єктів звичайний postMessage() простіший і безпечніший.
MessageChannel створює пару пов’язаних портів:
повідомлення, надіслане в один порт, надходить у другий;
один порт можна передати worker через transferList;
після цього головний потік і worker можуть спілкуватися через цей окремий канал.
// message-channel.js
const {
Worker,
MessageChannel,
isMainThread,
parentPort
} = require('node:worker_threads');
if (isMainThread) {
const worker = new Worker(__filename);
const { port1, port2 } = new MessageChannel();
port1.on('message', (message) => {
console.log('Повідомлення з окремого каналу:', message);
port1.close();
worker.terminate();
});
worker.postMessage({ port: port2 }, [port2]);
port1.postMessage({ text: 'Запит через MessageChannel' });
} else {
parentPort.once('message', ({ port }) => {
port.on('message', (message) => {
port.postMessage({
received: message.text,
handledBy: 'worker'
});
});
});
}MessageChannel корисний, коли worker має кілька незалежних каналів, наприклад окремий канал для команд і окремий для подій.
Для запуску дочірнього Node.js-процесу з вбудованим IPC використовується fork() з модуля node:child_process.
Батьківський процес може:
викликати child.send(message);
слухати подію 'message'.
Дочірній процес може:
викликати process.send(message);
слухати подію 'message'.
На відміну від worker-потоку, дочірній процес має окрему пам’ять і окремий екземпляр Node.js. Комунікація відбувається через IPC-канал.
// process-message.js
const { fork } = require('node:child_process');
if (process.argv[2] === 'child') {
process.on('message', (message) => {
if (message.operation !== 'multiply') {
process.send({
error: 'Невідома операція'
});
return;
}
const result = message.values.reduce(
(product, value) => product * value,
1
);
process.send({
operation: message.operation,
result
});
});
} else {
const child = fork(__filename, ['child']);
child.on('message', (message) => {
console.log('Батьківський процес отримав:', message);
child.disconnect();
});
child.on('error', (error) => {
console.error('Помилка дочірнього процесу:', error);
});
child.on('exit', (code, signal) => {
console.log(`Дочірній процес завершено: code=${code}, signal=${signal}`);
});
child.send({
operation: 'multiply',
values: [2, 3, 5]
});
}Запуск:
node process-message.jsУ цьому прикладі:
батьківський процес запускає копію поточного файлу;
аргумент child переводить копію в режим дочірнього процесу;
батько надсилає дані через child.send();
дочірній процес відповідає через process.send();
після отримання відповіді батько закриває IPC-з’єднання через child.disconnect().
Повідомлення краще будувати як явно описані команди або події.
Наприклад:
{
type: 'user.created',
requestId: 'req-42',
payload: {
userId: 17
}
}Таке повідомлення має зрозумілу структуру:
type визначає тип операції або події;
requestId дає змогу пов’язати запит із відповіддю;
payload містить дані операції.
Для відповідей можна використовувати окремі поля:
{
type: 'user.created.result',
requestId: 'req-42',
ok: true,
payload: {
userId: 17
}
}А для помилки:
{
type: 'user.created.result',
requestId: 'req-42',
ok: false,
error: {
code: 'VALIDATION_ERROR',
message: 'Некоректний ідентифікатор користувача'
}
}Це краще за передавання невизначених об’єктів, оскільки отримувач може перевірити тип і безпечно обробити повідомлення.
Вибір залежить від ізоляції, вартості запуску та типу роботи.
потрібно виконувати CPU-інтенсивні JavaScript-операції;
робота має виконуватися в межах одного застосунку;
важлива менша вартість обміну даними;
потрібно використовувати MessageChannel або передавання буферів;
достатньо ізоляції на рівні потоку.
Worker-потік ділить процес із головним потоком, тому помилка або небезпечна поведінка в процесі можуть вплинути на весь процес Node.js.
потрібна сильніша ізоляція пам’яті;
запускається окремий Node.js-застосунок;
процес має власні змінні середовища, аргументи або життєвий цикл;
потрібно ізолювати аварійне завершення;
окремий компонент має працювати незалежно від головного процесу.
Дочірні процеси зазвичай важчі за worker-потоки: запуск нового процесу потребує більше ресурсів, а обмін через IPC не дає спільної пам’яті.
Обчислення всередині одного Node.js-застосунку — worker-потік.
Окремий ізольований компонент або програма — дочірній процес.
Великий ArrayBuffer, який не потрібно копіювати — worker-потік із transferList.
Комунікація між незалежними Node.js-процесами — IPC через fork().
Після завершення роботи потрібно коректно закривати ресурси:
для worker — викликати worker.terminate(), якщо він більше не потрібен;
для процесу з IPC — викликати child.disconnect(), коли обмін завершено;
слухати події 'error', 'exit' або 'close';
не очікувати відповіді після примусового завершення worker чи процесу.
Якщо worker має активний обробник повідомлень, він може залишатися запущеним і не завершуватися самостійно. Визначте протокол завершення, наприклад команду { type: 'shutdown' }, або завершуйте worker із головного потоку.
postMessage()Отримувач отримує клон об’єкта, а не той самий об’єкт у пам’яті. Не покладайтеся на спільну змінність звичайних об’єктів.
Якщо потрібен спільний стан, необхідно використовувати спеціальний механізм, наприклад SharedArrayBuffer, і синхронізацію доступу до нього.
Функції не передаються через стандартний структурований клон. Замість функції передавайте дані та назву операції:
{
operation: 'resize',
width: 800,
height: 600
}Отримувач сам вибирає реалізацію операції за значенням operation.
transferListЯкщо великий ArrayBuffer передано без transferList, Node.js може скопіювати його. Для великих даних це збільшує використання пам’яті та час передавання.
Після передавання ArrayBuffer відправник втрачає доступ до його вмісту. Не читайте й не змінюйте такий буфер у відправника.
Обробник 'message' не замінює обробник помилок. Додавайте обробку:
worker.on('error', ...);
child.on('error', ...);
завершення worker або процесу через 'exit'.
Якщо різні частини програми надсилають об’єкти з різною структурою, обробка швидко стає ненадійною. Визначте формат повідомлення з полем типу, ідентифікатором запиту та даними.
Worker-потоки обмінюються повідомленнями через postMessage() і parentPort.
Дочірні процеси, створені через fork(), використовують child.send() і process.send().
Звичайні дані передаються копіюванням.
Великі ArrayBuffer можна передати без копіювання через transferList, але після цього відправник втрачає буфер.
MessageChannel дає змогу створювати окремі канали між worker-потоками.
Worker-потоки зручні для обчислень у межах одного процесу.
Дочірні процеси забезпечують сильнішу ізоляцію та незалежний життєвий цикл.
Надійний протокол повідомлень має визначені типи, дані, ідентифікатори запитів і формат помилок.