人人都会AI编程

线程间通信、共享内存

更新时间:2026-07-11

worker_threads 的真正威力不仅在于它能并行执行 CPU 密集型任务,更在于它提供了高效且安全的线程间数据交换机制。Node.js 为工作线程准备了两种主要的通信路径:基于消息端口的显式消息传递,以及基于共享内存的直接数据共享。两者在适用场景和性能特点上有明显差异。

1. 消息传递:postMessageMessagePort

每个工作线程都拥有一个 parentPort(主线程侧的对应端口是 worker.postMessage)。主线程和 Worker 可以像下面这样互发消息:

主线程(main.js)

const { Worker } = require('worker_threads');

const worker = new Worker('./worker.js');
worker.on('message', (msg) => {
  console.log('主线程收到:', msg);
});
worker.postMessage({ task: 'compute', params: [1, 2] });

工作线程(worker.js)

const { parentPort } = require('worker_threads');

parentPort.on('message', (msg) => {
  console.log('Worker 收到:', msg);
  // 执行任务后回复
  parentPort.postMessage({ result: msg.params.reduce((a, b) => a + b, 0) });
});

通过 postMessage 发送的对象会经过结构化克隆(structured clone)算法进行深拷贝。这保证了发送方和接收方持有的对象完全独立,不会意外相互影响。你可以像复制普通对象一样传递多数内置类型(Object、Array、Map、Set、Date、TypedArray 等),但函数、DOM 对象、WeakMap 等无法被克隆。

2. 高性能传输:利用 Transferable 对象转移所有权

当需要传递大块二进制数据(如图像、文件缓冲区)时,深拷贝会带来不必要的内存和 CPU 开销。Node.js 允许将 ArrayBufferTypedArray 作为 可转移对象(Transferable)传递。此时,内存的所有权会直接从发送方转移到接收方,发送方将无法再访问该数据,避免了复制过程。

转移缓冲区的例子:

// 主线程
const buf = Buffer.alloc(1024 * 1024); // 1 MB
buf.fill(1);
worker.postMessage(buf, [buf.buffer]); // 转移底层 ArrayBuffer

// Worker 接收
parentPort.on('message', (msg) => {
  // msg 现在是同一个 Buffer,但所有权已转移,原 buf 在主线程失效
  console.log(msg[0]); // 1
});

第二个参数 [buf.buffer] 是转移列表,告诉引擎将 buf.buffer 的所有权交给 Worker。这种机制在音视频处理、加密解密、大数据交换场景中能显著提升性能。

3. 共享内存:SharedArrayBufferAtomics

消息传递是“传数据”的方式,共享内存则是“共享数据”的方式。SharedArrayBuffer 允许多个线程直接读写同一块内存区域,配合 Atomics 对象可以进行原子操作,确保数据一致性——无需任何锁。

共享数组的使用:

// 主线程
const { Worker } = require('worker_threads');

const sharedBuffer = new SharedArrayBuffer(4); // 4 字节
const sharedArray = new Int32Array(sharedBuffer);
sharedArray[0] = 0;

const worker = new Worker('./worker.js', {
  workerData: sharedBuffer
});
worker.on('message', () => {
  console.log('最终值:', sharedArray[0]);
});

worker.js

const { parentPort, workerData } = require('worker_threads');
const sharedArray = new Int32Array(workerData);

// 原子操作,不必担心竞态
Atomics.add(sharedArray, 0, 1); // 等价于 sharedArray[0] += 1,但是原子的
parentPort.postMessage('done');

原子操作包括 addsubandorxorcompareExchange 等。当多个 Worker 同时修改同一位置时,这些操作保证不会出现读-修改-写的中断问题。

线程等待与唤醒Atomics.wait()Atomics.notify() 提供轻量级的同步原语。Worker A 可以进入等待状态,直到另一个线程通知它继续。注意,为了避免死锁,Atomics.wait 不能在 Node.js 的主线程中使用(主线程不支持),只能在 Worker 中使用,主线程更适合用异步消息协调。

4. 实际使用中的注意事项

  • 结构化克隆的边界:如果你尝试传递一个含有无法克隆对象的复杂图(如文件描述符、WritableStream),postMessage 会抛出异常。必要时将数据转换为 JSON 或可序列化格式再发送。
  • 共享内存的安全风险SharedArrayBuffer 在浏览器环境中受限于跨域隔离策略,在 Node.js 中虽然可以自由使用,但不当的并发写入(不使用 Atomics)会引发难以调试的竞态条件。永远使用 Atomics 进行写入操作。
  • 内存生命周期:共享内存不会被 V8 的垃圾回收自动释放——它一直存在,直到所有引用它的线程都断开连接。要小心内存泄漏,及时终止 Worker 或主动释放引用。
  • cluster 的取舍worker_threads 因为共享内存和轻量特性,更适合 CPU 密集型并行计算(如排序、图像处理、复杂数学运算)。而 cluster 多进程是独立进程,内存隔离,但通信成本更高,更适合 I/O 密集型 Web 服务的多核负载均衡

线程间通信和共享内存是 worker_threads 模块的核心能力,它们使得 Node.js 在面对计算密集型任务时不再束手束脚。理解消息传递的复制/转移机制,以及共享内存的原子化操作,可以帮助你在需要并行计算的场景中写出既安全又高效的后端代码。