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。

跨平台注意事项

常见错误

练习

  1. 为以下任务选择技术并说明理由:压缩视频、计算斐波那契、运行 Python、运行四个 Express 实例、可靠处理订单。
  2. 分别输出 Cluster Worker 和 Worker Thread 的 process.pid,观察差异。
  3. fork() 建立 IPC,再用 Worker Thread 的 postMessage() 实现同一计算,对比通信模型。

验收清单