Node.js 真·多线程 Worker Threads 初探

6,650 阅读3分钟

基本信息

笔者在 Node.js 最新的开发版本 v11.4.0 上测试该特性,目前需要添加 flag 才能引入 Worker Threads,例如:

node --experimental-worker index.js

Worker Threads 特性是在2018年6月20日的 v10.5.0 版本引入的:

node/CHANGELOG

目前该模块处于 Stability 1 - Experimental 阶段,改动会较大,不建议用于生产环境。

模块接口

const {
  isMainThread, parentPort, workerData, threadId,
  MessageChannel, MessagePort, Worker
} = require('worker_threads');

该模块对象和类非常少,只有4个对象和3个类。

  • isMainThread:false 表示当前为 worker 线程,false 表示为主线程
  • parentPort: 在 worker 线程里是表示父进程的 MessagePort 类型的对象,在主线程里为 null
  • workerData: 在 worker 线程里是父进程创建 worker 线程时的初始化数据,在主线程里是 undefined
  • threadId: 在 worker 线程里是线程 ID,在父进程里是 0
  • MessageChannel: 包含两个已经互相能够夸线程通信的 MessagePort 类型对象,可用于创建自定义的通信频道,可参考样例二的实现。
  • MessagePort: 用于跨线程通信的句柄,继承了 EventEmitter,包括 close message 事件用于接收对象关闭和发送的消息,以及 close postMessage 等操作。
  • Worker: 主线程用于创建 worker 线程的对象类型,包含所有的 MessagePort 操作以及一些特有的子线程 meta data 操作。构造函数的第一个参数是子线程执行的入口脚本程序,第二个参数包含一些配置项,可以指定一些初始参数。 详细内容见文档

内存模型的变更

在使用 clusterchild_process 时通常使用 SharedArrayBuffer 来实现需要多进程共享的内存。

port.postMessage(value[, transferList])

现在 Worker Threads 模块在 API 层不建议多线程共享内存,第一个参数 value 的值会被 clone 一份在接受消息的线程。transferList 只能传递 ArrayBuffer 或者 MessagePort 对象,传递 ArrayBuffer 会修改该 Buffer 的访问权限给接受消息的线程,传递 MessagePort 可以参考样例二。

所有跨线程消息的通信都通过走底层的 v8 序列化实现,更具体的 Worker Threads 和 v8 多线程模型以及和浏览器的 Web Worker 标准的关系暂不展开。

样例一:主线程和 worker 线程通信计数,计数到 5 后 worker 线程自杀

const {
  isMainThread, parentPort, workerData, threadId,
  MessageChannel, MessagePort, Worker
} = require('worker_threads');

function mainThread() {
  const worker = new Worker(__filename, { workerData: 0 });
  worker.on('exit', code => { console.log(`main: worker stopped with exit code ${code}`); });
  worker.on('message', msg => {
    console.log(`main: receive ${msg}`);
    worker.postMessage(msg + 1);
  });
}

function workerThread() {
  console.log(`worker: threadId ${threadId} start with ${__filename}`);
  console.log(`worker: workerDate ${workerData}`);
  parentPort.on('message', msg => {
    console.log(`worker: receive ${msg}`);
    if (msg === 5) { process.exit(); }
    parentPort.postMessage(msg);
  }),
  parentPort.postMessage(workerData);
}

if (isMainThread) {
  mainThread();
} else {
  workerThread();
}

输出结果:

worker: threadId 1 start with /Users/azard/test/index.js
worker: workerDate 0
main: receive 0
worker: receive 1
main: receive 1
worker: receive 2
main: receive 2
worker: receive 3
main: receive 3
worker: receive 4
main: receive 4
worker: receive 5
main: receive 5
main: worker stopped with exit code 0

样例二:使用 MessageChannel 让两个子线程直接通信

const {
  isMainThread, parentPort, workerData, threadId,
  MessageChannel, MessagePort, Worker
} = require('worker_threads');

if (isMainThread) {
  const worker1 = new Worker(__filename);
  const worker2 = new Worker(__filename);
  const subChannel = new MessageChannel();
  worker1.postMessage({ hereIsYourPort: subChannel.port1 }, [subChannel.port1]);
  worker2.postMessage({ hereIsYourPort: subChannel.port2 }, [subChannel.port2]);
} else {
  parentPort.once('message', (value) => {
    value.hereIsYourPort.postMessage('hello');
    value.hereIsYourPort.on('message', msg => {
      console.log(`thread ${threadId}: receive ${msg}`);
    });
  });
}

输出:

thread 2: receive hello
thread 1: receive hello

总结

现在 Node.js 有了真多线程,不需要再用 cluster 或者 child_process 的多进程来处理一些问题了,相关的框架、库的模型在 Worker Threads 稳定后也可以开始进行迭代更新。