15. Worker、child_process、cluster 与 Worker Threads
本章解决的问题
worker 只是“执行工作者”的角色名称,不是 Node.js 中唯一、固定的技术。看到 worker 变量名时,必须继续确认它究竟是进程、线程,还是某个任务队列的消费者。
Worker 可能指什么
worker
├─ Cluster Worker:提供网络服务的 Node.js 子进程
├─ Worker Thread:同一进程中的 JavaScript 线程
├─ child_process Worker:团队对普通子进程的业务命名
├─ BullMQ Worker:消费 Redis 队列任务的逻辑角色
└─ Web Worker:浏览器中的后台线程
因此,“启动四个 worker”这句话信息不足。应继续问:使用哪个 API、隔离单位是什么、是否共享内存、由谁负责重启?
四种 Node.js 能力的关系
| 技术 | 隔离单位 | 内存 | 主要通信 | 高频用途 |
|---|---|---|---|---|
child_process.spawn() |
操作系统进程 | 不共享 | stdio | Bash、Python、FFmpeg、大输出程序 |
child_process.fork() |
Node.js 进程 | 不共享 | IPC + stdio | Node.js 后台子任务 |
cluster |
Node.js 进程 | 不共享 | Cluster IPC | 多进程共享网络服务 |
worker_threads |
同一进程内的线程 | 默认隔离,可共享 | message、SharedArrayBuffer | CPU 密集型 JavaScript |
Cluster Worker 本质上是由 Node.js 创建的子进程。cluster.fork() 与普通 child_process.fork() 有亲缘关系,但 Cluster 额外提供了 Primary/Worker 生命周期、共享服务端口和连接分发语义。
如何选择
需要运行 Python、Bash 或系统命令?
└─ child_process.spawn()/execFile()
需要运行独立 Node.js 子任务并使用 IPC?
└─ child_process.fork()
需要一个 Node.js HTTP 服务利用多个 CPU 核心?
└─ 部署层多实例,或 cluster
需要加速 CPU 密集的纯 JavaScript 计算?
└─ worker_threads,通常配线程池
需要可靠后台任务、重试、跨机器扩容?
└─ 队列 Worker;它内部仍可能调用 child_process
三个最小示例
普通子进程
import { spawn } from "node:child_process";
const child = spawn("python", ["scripts/analyze.py", "--input", "data.csv"]);
child.stdout.pipe(process.stdout);
child.stderr.pipe(process.stderr);
它适合调用外部程序,不能直接执行父进程中的某个 TypeScript 函数。
Cluster Worker
import * as cluster from "node:cluster";
if (cluster.isPrimary) {
cluster.fork();
} else {
console.log(`Cluster Worker PID=${process.pid}`);
}
每个 Worker 都是独立 Node.js 进程,会重新执行入口模块。
Worker Thread
import { Worker, isMainThread, parentPort } from "node:worker_threads";
if (isMainThread) {
const worker = new Worker(__filename);
worker.on("message", (value: unknown) => console.log(value));
} else {
parentPort?.postMessage({ result: 42 });
}
当前项目由 TypeScript 编译为 CommonJS。与 fork() 一样,生产环境通常让 Worker 执行编译后的 JavaScript 文件。线程执行 CPU 密集任务时不会阻塞主线程的 Event Loop,但仍会竞争同一进程和机器的 CPU、内存资源。
隔离与故障边界
进程的隔离更强:子进程崩溃通常不会直接破坏父进程的 JavaScript 堆;但创建成本、内存开销和通信成本更高。
Worker Thread 更轻,允许使用 SharedArrayBuffer 共享内存;但共享内存会引入竞态条件和同步难题。初学者不要因为“更快”就把 HTTP 服务改成线程共享状态。
进程:独立 PID、独立 V8 堆、独立 Event Loop
线程:同一 PID、每个 Worker 有自己的 JS 执行环境,可显式共享内存
“Node.js 单线程”该怎样理解
通常是说一个 Node.js JavaScript 主执行环境在一个线程上运行事件循环,并不代表整个 Node.js 进程只有一个线程,也不代表应用不能创建子进程或 Worker Thread。libuv 线程池、运行时内部线程、Worker Thread 和 Cluster Worker 是不同层次。
时间线对比
child_process:父进程 → spawn 外部可执行文件 → stdio/exit/close
fork: 父进程 → 启动 Node.js 子模块 → IPC message → close
cluster: Primary → fork Worker → online → listening → 分发连接
worker_threads:主线程 → new Worker → online → message → exit
事件名称相似不代表语义完全相同,使用时应查看对应对象的 API。
跨平台注意事项
- 进程创建、可执行文件名、Shell、Signal、路径和权限在 Windows/Linux 间有差异。
- Worker Threads 的 API 跨平台更一致,但执行文件路径仍需正确解析。
- Cluster 的连接调度默认策略可能因平台而不同。
- 不要在 Windows 上假设存在
bash、python3或 POSIX Signal。
常见错误
- 把 Cluster Worker 与 Worker Thread 当成同一个东西。
- 为 CPU 密集 JavaScript 使用多个异步 Promise,误以为会自动并行。
- 为调用 Python 使用 Worker Thread;线程不能替代外部 Python 进程。
- 把 Cluster 当作后台任务队列;它不提供持久化、重试和跨机器协调。
- 认为进程之间的全局
Map、Session 或限流器会共享。 - 同时启用 PM2 Cluster 和应用内 Cluster,导致进程倍增。
练习
- 为以下任务选择技术并说明理由:压缩视频、计算斐波那契、运行 Python、运行四个 Express 实例、可靠处理订单。
- 分别输出 Cluster Worker 和 Worker Thread 的
process.pid,观察差异。 - 用
fork()建立 IPC,再用 Worker Thread 的postMessage()实现同一计算,对比通信模型。
验收清单
- [ ] 能解释
worker为什么只是角色名。 - [ ] 能区分
spawn()、fork()、Cluster Worker 和 Worker Thread。 - [ ] 能说明 Cluster Worker 是独立进程且不共享变量。
- [ ] 能针对外部程序、CPU 计算、HTTP 扩容和队列任务做选择。
- [ ] 不会用 Cluster 代替持久化任务队列。