Node.js Worker Threads 多线程实战指南
CPU 密集任务会阻塞 Node.js 事件循环。本文讲解 Worker Threads 的原理、实战用法、线程池模式与性能优化,让 CPU 任务不再拖垮服务。
Node.js 单线程只擅长 IO 密集任务;CPU 密集计算(图像处理、数据加解密、大数运算)用 worker_threads 模块放到独立线程:new Worker('./task.js', { workerData }) 启动,postMessage 通信。多任务场景用线程池复用 Worker。
IO 密集 vs CPU 密集
事件循环的魔法只覆盖 IO:数据库查询、网络请求发出后线程空闲处理别的。但 CPU 计算没有"发出后等待"——一个 100ms 的同步计算,事件循环就停摆 100ms,期间所有请求排队。这就是 CPU 任务必须离开主线程的原因。
感受问题:主线程跑 CPU 任务
// 斐波那契这种递归计算是纯 CPU 任务
function fib(n) { return n < 2 ? n : fib(n - 1) + fib(n - 2); }
app.get('/fib/:n', (req, res) => {
res.json({ result: fib(Number(req.params.n)) }); // fib(45) 阻塞数秒!
});
一个 /fib/45 请求期间,整个服务对所有用户无响应——一个端点打死整站。
Worker Threads 改造
// worker.js:计算线程
const { parentPort, workerData } = require('node:worker_threads');
parentPort.postMessage({ result: fib(workerData.n) });
// 主线程
const { Worker } = require('node:worker_threads');
app.get('/fib/:n', async (req, res) => {
const worker = new Worker('./worker.js', { workerData: { n: Number(req.params.n) } });
worker.once('message', ({ result }) => res.json({ result }));
worker.once('error', (err) => res.status(500).json({ error: err.message }));
});
主线程继续处理请求,计算在独立线程进行——服务响应能力不受 CPU 任务影响。
线程池模式
每请求新建 Worker 开销太大(线程创建 ~ms 级 + 独立 V8 实例内存)。生产用线程池:
class WorkerPool {
constructor(size, workerFile) {
this.workers = Array.from({ length: size }, () => new Worker(workerFile));
this.idle = [...this.workers];
this.queue = [];
}
run(data) {
return new Promise((resolve, reject) => {
const task = { data, resolve, reject };
const worker = this.idle.pop();
if (worker) this.dispatch(worker, task);
else this.queue.push(task); // 排队等空闲线程
});
}
dispatch(worker, task) {
worker.once('message', (result) => {
this.idle.push(worker);
task.resolve(result);
if (this.queue.length) this.dispatch(this.idle.pop(), this.queue.shift());
});
worker.postMessage(task.data);
}
}
池大小 = CPU 核数(计算密集时)。成熟库:Piscina(pool 管理开箱即用)。
优化要点
- 通信成本:postMessage 是结构化克隆序列化——大数据传递用 SharedArrayBuffer 或 Transferable 对象零拷贝;
- 任务粒度:太碎的任务通信开销盖过并行收益;
- 内存:每个 Worker 独立 V8 堆(几十 MB 起步),池大小要算内存账。
常见问题(FAQ)
Q:Worker Threads 和 cluster 怎么分工?
cluster:多进程分摊请求负载(IO 吞吐扩容);Worker Threads:单进程内卸载CPU 计算。两者可同时用:多进程各自带线程池。
Q:什么任务值得 Worker?
执行时间 >50ms 的同步计算(图像处理、压缩、加密、PDF 生成、大数据集转换)。毫秒级任务通信开销不划算。
Q:Node 跑 CPU 密集服务是不是选错语言?
Worker Threads + 原生模块(Rust/Go 写的 N-API 扩展)让 Node 能扛相当规模。但 CPU 密集是主业的服务(视频转码集群),直接选 Go/Rust 更务实。
参考文档
本文依据公开文档整理,示例需按项目依赖版本验证;不将文档中的性能描述视为本项目实测结果。