创建工作线程与传递消息
创建工作线程,通过 postMessage 交换数据,并使用 workerData 和 MessageChannel 进行结构化通信。
创建工作线程与传递消息 是 CoddyKit 上的免费 Node.js Backend Development Bootcamp 课时。 这是第 2 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 Node.js Backend Development Bootcamp 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 Node.js Backend Development Bootcamp 课程共包含 4 节课。
本课时的部分内容尚未翻译,以英文显示。
Why Worker Threads Exist
Node.js runs your JavaScript on a single thread. That is great for I/O-bound work (HTTP, DB, files) because the event loop never blocks. But a CPU-bound task — image resizing, hashing, parsing huge payloads, crypto — runs synchronously and freezes the entire server until it finishes.
- I/O-bound → stay on the event loop, use async APIs.
- CPU-bound → offload to a
Workerthread so the main thread keeps serving requests.
The worker_threads module lets you run JavaScript in parallel on separate OS threads, each with its own V8 isolate and event loop.
Spawning Your First Worker
You create a thread with new Worker(filename). The file runs on a fresh thread. Use isMainThread to make one file behave differently depending on where it runs.
Below, the main thread spawns a worker that points back to the same file. The worker branch does the work and exits.
const { Worker, isMainThread } = require('node:worker_threads');
if (isMainThread) {
console.log('main: spawning worker');
const worker = new Worker(__filename);
worker.on('exit', (code) => console.log('worker exited with', code));
} else {
// This branch runs inside the worker thread
let sum = 0;
for (let i = 0; i < 1e7; i++) sum += i;
console.log('worker computed sum =', sum);
}Sending Data In with workerData
To hand input to a worker at creation time, pass workerData in the options object. Inside the worker, read it from the worker_threads module.
workerDatais cloned (structured clone), not shared — mutating it in the worker does not affect the main thread.- Use it for the initial job parameters: a file path, a number range, config.
const { Worker, isMainThread, workerData } = require('node:worker_threads');
if (isMainThread) {
new Worker(__filename, { workerData: { start: 1, end: 5 } });
} else {
const { start, end } = workerData;
let product = 1;
for (let i = start; i <= end; i++) product *= i;
console.log('factorial-ish product =', product);
}Getting Results Back with postMessage
workerData is one-way and one-time. For results, use the message channel that every worker has built in:
- Inside the worker:
parentPort.postMessage(value)sends data to the parent. - On the main thread:
worker.on('message', cb)receives it.
Messages are asynchronous and structured-cloned, so the main thread stays responsive while the worker computes.
const { Worker, isMainThread, parentPort, workerData } = require('node:worker_threads');
if (isMainThread) {
const worker = new Worker(__filename, { workerData: 40 });
worker.on('message', (result) => {
console.log('main received:', result);
});
} else {
function fib(n) { return n < 2 ? n : fib(n - 1) + fib(n - 2); }
parentPort.postMessage(fib(workerData));
}Two-Way Messaging
The channel works both directions. The parent can worker.postMessage() and the worker listens with parentPort.on('message', ...). This turns a worker into a long-lived service that processes many jobs instead of one.
Notice the worker stays alive listening for messages; you must explicitly tell it to stop (here, a null sentinel triggers process.exit).
const { Worker, isMainThread, parentPort } = require('node:worker_threads');
if (isMainThread) {
const worker = new Worker(__filename);
worker.on('message', (r) => console.log('squared ->', r));
[2, 3, 4].forEach((n) => worker.postMessage(n));
worker.postMessage(null); // sentinel to stop
} else {
parentPort.on('message', (n) => {
if (n === null) { process.exit(0); }
parentPort.postMessage(n * n);
});
}Promisifying a Single Job
In backend code you usually want a clean async function: call it, await a result. Wrap the worker lifecycle in a Promise, resolving on message and rejecting on error or a non-zero exit.
This pattern is the foundation of any worker-pool: one promise per task.
const { Worker, isMainThread, parentPort, workerData } = require('node:worker_threads');
function runJob(payload) {
return new Promise((resolve, reject) => {
const worker = new Worker(__filename, { workerData: payload });
worker.on('message', resolve);
worker.on('error', reject);
worker.on('exit', (code) => {
if (code !== 0) reject(new Error('exit code ' + code));
});
});
}
if (isMainThread) {
runJob([5, 10, 15]).then((sum) => console.log('total =', sum));
} else {
const total = workerData.reduce((a, b) => a + b, 0);
parentPort.postMessage(total);
}Structured Clone: What You Can Send
Messages are copied using the structured clone algorithm, not JSON.stringify. That means you can send more than plain JSON.
- Supported: objects, arrays,
Map,Set,Date,RegExp,ArrayBuffer, typed arrays,BigInt. - Not supported: functions, class instances with methods, DOM-like objects, anything with closures — these throw a
DataCloneError.
Cloning large objects costs CPU and memory. For big binary buffers, prefer transferring instead of copying (next scene).
Transferring Buffers Instead of Copying
For large binary data, copying is wasteful. Pass a transfer list as the second argument to postMessage. Ownership of the ArrayBuffer moves to the other thread — zero-copy — and the buffer becomes unusable on the sender (its byteLength becomes 0).
Use this for image bytes, file chunks, or any megabyte-scale payload to avoid duplicating memory.
const { Worker, isMainThread, parentPort, workerData } = require('node:worker_threads');
if (isMainThread) {
const buf = new Uint8Array([1, 2, 3, 4]).buffer;
const worker = new Worker(__filename, { workerData: buf }, { transferList: [buf] });
worker.on('message', (sum) => console.log('byte sum =', sum));
} else {
const bytes = new Uint8Array(workerData);
let sum = 0;
for (const b of bytes) sum += b;
parentPort.postMessage(sum);
}MessageChannel for Side Channels
Beyond the built-in parentPort, you can create your own channel with new MessageChannel(). It gives two linked ports, port1 and port2. Keep one, transfer the other into a worker, and now you have a dedicated pipe — useful for separating control messages from data, or for worker-to-worker communication.
A MessagePort is itself transferable, so you send it inside workerData or a message with a transfer list.
const { Worker, isMainThread, MessageChannel, parentPort, workerData } =
require('node:worker_threads');
if (isMainThread) {
const { port1, port2 } = new MessageChannel();
new Worker(__filename, { workerData: { port: port2 }, transferList: [port2] });
port1.on('message', (msg) => {
console.log('main got on side channel:', msg);
port1.close();
});
port1.postMessage('ping');
} else {
const { port } = workerData;
port.on('message', (msg) => {
port.postMessage('pong (you said: ' + msg + ')');
});
}Handling Errors and Clean Shutdown
A worker can crash. Always wire up its lifecycle events so a failed job does not silently hang a request:
'error'— an uncaught exception inside the worker.'exit'— fired once;codeis non-zero on failure.'messageerror'— a message could not be deserialized.
To stop a worker from the main thread, call worker.terminate() (returns a promise). Unterminated workers keep the process alive.
const { Worker, isMainThread, parentPort } = require('node:worker_threads');
if (isMainThread) {
const worker = new Worker(__filename);
worker.on('error', (err) => console.log('caught worker error:', err.message));
worker.on('exit', (code) => console.log('exited, code =', code));
} else {
throw new Error('boom inside worker');
}A Practical Backend Shape
In a real Node.js backend you do not spawn a worker per request — thread startup is expensive. Instead:
- Keep a small pool of long-lived workers (often
os.cpus().length). - Send each CPU-bound job over
postMessageand match results by anid. - Reserve workers for genuinely CPU-bound work; leave I/O on the event loop.
This snippet shows the per-worker message protocol using an id to correlate request and response — the core idea every pool library (like piscina) builds on.
const { Worker, isMainThread, parentPort } = require('node:worker_threads');
if (isMainThread) {
const worker = new Worker(__filename);
const pending = new Map();
let nextId = 0;
worker.on('message', ({ id, result }) => {
pending.get(id)(result);
pending.delete(id);
if (pending.size === 0) worker.terminate();
});
function submit(payload) {
return new Promise((res) => {
const id = nextId++;
pending.set(id, res);
worker.postMessage({ id, payload });
});
}
Promise.all([submit(3), submit(7)]).then((r) => console.log('results:', r));
} else {
parentPort.on('message', ({ id, payload }) => {
parentPort.postMessage({ id, result: payload * payload });
});
}Quick Check
You must send a 50 MB image buffer to a worker for processing and you want to avoid duplicating that memory. Which approach is correct?
Recap
You now know how to spawn workers and move data between threads:
- Spawn with
new Worker(file, { workerData }); branch onisMainThread. - Send results with
parentPort.postMessage()and receive viaworker.on('message')— the channel is two-way. - Messages use structured clone (richer than JSON, but no functions); large buffers should be transferred via a transfer list for zero-copy.
- Create extra pipes with
MessageChanneland transfer aMessagePortfor side or worker-to-worker channels. - Always handle
error/exitand callterminate(); in production use a pool with id-correlated messages rather than one worker per request.
常见问题解答
「创建工作线程与传递消息」课时是免费的吗?
是的 — 「创建工作线程与传递消息」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 Node.js Backend Development Bootcamp 课程的其余内容,请升级到 CoddyKit PRO。 Node.js Backend Development Bootcamp 课程共包含 4 节课。
「创建工作线程与传递消息」这节课中我会学到什么?
创建工作线程,通过 postMessage 交换数据,并使用 workerData 和 MessageChannel 进行结构化通信。 你通过在浏览器中直接运行的动手代码来练习 Node.js Backend Development Bootcamp,全天候 AI 导师会在你学习这节课的过程中回答你的问题。
学习 Node.js Backend Development Bootcamp 需要有经验吗?
无需任何先前经验。CoddyKit 上的 Node.js Backend Development Bootcamp 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 2 节课,共 4 节。
「创建工作线程与传递消息」课时需要多长时间?
大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。
我能在这节 Node.js Backend Development Bootcamp 课中编写并运行代码吗?
能。每节 Node.js Backend Development Bootcamp 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。