Skip to main content

多进程与多线程

Node.js 的核心特性是单线程异步非阻塞 I/O。这使得它在处理 I/O 密集型任务(如高并发的 HTTP 请求、数据库读写)时表现极其出色。

然而,单线程也带来了两个致命缺陷:

  1. 无法利用多核 CPU:一台 8 核服务器,Node.js 默认只能使用 1 个核,造成极大资源浪费。
  2. 不擅长 CPU 密集型任务:如果执行一段非常耗时的同步计算(如巨量数组排序、加密解密、复杂的正则匹配),整个事件循环(Event Loop)会被阻塞,导致所有其他用户的请求都无法被响应。

为了解决这两个问题,Node.js 提供了完善的多进程child_process, cluster)和多线程worker_threads)方案。


多进程 (Multi-process)

多进程架构主要用来解决利用多核 CPU提升服务稳定性的问题。

child_process 模块

Node 提供了四个核心 API 用于创建子进程:

  1. exec:衍生一个 shell 来运行命令,将输出缓存起来后回调返回。适合执行简单的系统命令(输出量小)。
  2. execFile:直接执行可执行文件,不衍生 shell,性能比 exec 高,更安全。
  3. spawn:以流(Stream)的方式实时返回数据。适合处理海量数据或长时间运行的任务。
  4. fork(最高频):专门用来衍生新的 Node.js 进程。它不仅衍生进程,还会在父子进程之间自动建立一条 IPC(进程间通信)通道,可以直接使用 process.send()process.on('message') 进行通信。

Cluster 模块与端口共享机制

在生产环境中部署 Node.js 服务时,通常会使用 Cluster 模块(或者基于它的 PM2)来启动多个进程。

🔥 面试题:Node.js 是如何实现多个进程监听同一个端口的?为什么不会报“端口被占用(EADDRINUSE)”?

这是一个区分度极高的底层面试题。

  • 常规认知:操作系统的网络模型中,一个端口在同一时刻只能被一个进程绑定监听。如果子进程直接 listen(3000),必定报错。
  • Node.js 底层原理
    1. 在 Cluster 模式下,真正监听底层端口的只有主进程(Master),它会在内部创建一个 TCP 服务器。
    2. 子进程(Worker)里面调用的 server.listen(3000) 其实是被 Node.js 拦截和 hack 过的,它并不会真正在底层去调用操作系统的 bind/listen
    3. 主进程接收到外部的网络请求连接后,会根据指定的负载均衡算法(通常是 Round-Robin 轮询),通过 IPC 通道将这个 socket 句柄(Handle)发送给某一个空闲的子进程去处理。
    4. 这种底层机制叫做句柄传递(依赖操作系统的 sendmsg 系统调用)。因此,外部看来是多个进程在监听同一个端口,实际上只有 Master 占用了端口,Worker 只是拿到了主进程丢过来的句柄去处理请求。

IPC 通信原理

面试题:Node.js 父子进程是怎么通信的?

  • Node.js 的 IPC(Inter-Process Communication)底层是通过 Libuv 实现的。
  • 在 Windows 下基于 命名管道 (Named Pipe) 实现。
  • *nix (Linux/macOS) 下基于 Unix Domain Socket 实现。
  • 表现形式上:父子进程之间建立双向通信通道,传递的数据会被 JSON.stringify 序列化,另一端收到后再 JSON.parse 反序列化(因此无法传递函数或不可序列化的对象)。

多线程 (Multi-thread)

多进程虽然能利用多核,但进程非常重(每个进程都有自己独立的 V8 实例、事件循环和内存空间,起一个进程至少消耗 30MB 内存)。如果只是想算个复杂的数学题就起一个进程,代价太大了。

为了专门解决 CPU 密集型任务,Node.js 在 v10 之后引入了真正的多线程:worker_threads 模块

Worker Threads 的特点

  1. 共享内存:这是线程相比进程最大的优势!Worker Threads 可以通过 SharedArrayBuffer 实现多线程之间真正的内存共享,极大地避免了进程间通信序列化/反序列化的性能开销。
  2. 独立的执行环境:每个 Worker 线程有自己的 V8 引擎实例、Event Loop 事件循环。但它们同属于一个宿主进程,内存消耗远小于子进程。
  3. 通信机制:线程之间通过 MessageChannel (类似于前端 Web Worker 的 postMessage) 进行通信。

适用场景

  • 图像/视频处理
  • 大规模数据加解密
  • 极其复杂的 JSON 解析或数据过滤
// 多线程基础示例
const { Worker, isMainThread, parentPort } = require("worker_threads");

if (isMainThread) {
// 主线程
const worker = new Worker(__filename); // 将当前文件作为 Worker 运行
worker.on("message", (msg) => console.log("收到线程结果:", msg));
worker.postMessage("开始计算吧");
} else {
// Worker 线程
parentPort.on("message", (msg) => {
// 模拟一段极度耗时的 CPU 密集型计算
let count = 0;
for (let i = 0; i < 1000000000; i++) count++;
parentPort.postMessage(count);
});
}

面试总结:多进程 vs 多线程,怎么选?

🔥 面试题:在 Node.js 中,多进程和多线程有什么区别?在什么场景下分别使用?

对比维度多进程 (cluster / child_process)多线程 (worker_threads)
资源开销极大。每个进程是独立的 Node 实例,独占内存和 V8。起成百上千个会瞬间 OOM。较小。同一个进程下分配的线程,启动快,内存占用小。
数据共享默认内存隔离,无法共享。必须通过 IPC 传递,有序列化开销。可通过 SharedArrayBuffer 直接共享内存,通信效率极高。
稳定性极高。一个子进程崩溃(如 OOM),完全不影响其他进程和主进程。较弱。如果一个线程因为 C++ 层面的底层错误引发了 segment fault,可能会导致整个宿主进程崩溃。
核心适用场景Web 服务的负载均衡。用于开启多实例监听 HTTP 请求(如 PM2)。处理局部的 CPU 密集型任务。如加密计算、视频处理,算完就把结果返回给主线程。

一句话总结你的回答: “在 Node.js 的架构设计中,如果要提升 Web 服务的整体并发吞吐量,利用多核 CPU,我们应当使用 Cluster 模块(多进程);如果仅仅是为了防止某段复杂的 CPU 运算阻塞主事件循环,我们应当使用 worker_threads(多线程)将任务扔到后台去算。”