持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的15天,点击查看活动详情
码字不易,喜欢点赞,不喜请移步评论区告知,未经授权不得转载
一、前情回顾
上一篇小作文梳理了一遍从开始到 PoolWorker.prototype.run 方法的调用链路,然后分析得到 PoolWorker 实例后调用实例的 run 方法向 writePipe 写入数据的过程;
把数据写到 writePipe 标志着把任务数据发送给子进程。此时主进程中前半部分已经结束,这其中包含初始化管控 worker 进程的 WorkerPool 类实例、负责具体处理通信和创建子进程的 PoolWorker 类,这期间还穿插了 neo-async 和进程间通信的知识。
从本篇开始我们正式进入子进程的相关细节部分,如果前面的部分还有疑问,不推荐继续向后看,这回让你感到困惑和痛苦,请详细阅读前面九篇的基础(反正我不觉得基础,甚至有点高能)内容;
二、回顾创建子进程
创建子进程的逻辑是在 PoolWoker 类的构造函数中进行,所谓创建子进程就是调用 childProcess.spawn 创建一个执行 thread-loader/dist/woker.js 文件的子进程:
class PoolWorker {
constructor(options, onJobDone) {
const sanitizedNodeArgs = (options.nodeArgs || []).filter((opt) => !!opt);
// 创建子进程
this.worker = childProcess.spawn(
process.execPath, // node.js 在系统中的可执行文件的路径
[].concat(sanitizedNodeArgs).concat(
workerPath, // thread-loader 中的 worker.js 路径
options.parallelJobs // 传递给 worker.js 的进程参数 process.argv
),
{
detached: true, // 子进程独立运行需要此配置项
stdio: ['ignore', 'pipe', 'pipe', 'pipe', 'pipe'], // 进程间通信的管道
}
);
}
}
上面的示例代码说这么多其实等效于调用下面的命令:
# process.execPath 就是 node 命令
# workerPath 就是 thread-loader/dist/worker.js
# options.parallelJobs 就是 20,子进程并发任务数
$ node thread-loader/dist/worker.js 20
三、woker.js 文件代码结构
- 文件位置:
thread-loader/src/worker.js
这个 worker.js 是上面子进程运行的文件,loader 在这个文件中被调用。这里要说一下,thread-loader 被 webpack 引用的是被打包后输出到 dist 目录下的文件,这些文件被编译成 CommonJS 模块,这写文件不利于阅读,其源码都在 src 目录下,源码是遵循 ESModule 规范的模块和代码风格。
按照我们过往的经验先简化这些代码,下面的代码只保留函数名,函数体中的代码被省略了,这可以快速从宏观上先看看这个 js 文件都做了哪些工作、入口是哪里。
// import modules ignored ...
// 创建 fd:3/4 的流,他们作为进程间通信的基础
// fd:3 的可写流,子进程写入,父进程读取
const writePipe = fs.createWriteStream(null, { fd: 3 });
// fd: 4 的可读流,父进程写入,子进程读取
const readPipe = fs.createReadStream(null, { fd: 4 });
// 监听结束、关闭、错误事件,
writePipe.on('finish', onTerminateWrite);
writePipe.on('close', onTerminateWrite);
readPipe.on('end', onTerminateRead);
readPipe.on('close', onTerminateRead);
readPipe.on('error', onError);
writePipe.on('error', onError);
// 从进程参数获取并发任务数,默认 20
const PARALLEL_JOBS = +process.argv[2] || 20;
// 标识符,进程是否终止
let terminated = false;
// 问询 id,自增的
let nextQuestionId = 0;
// 回调函数对象,key 是对应的 nextQuestionId
const callbackMap = Object.create(null);
// 声明一堆方法
function onError(error) {}
function onTerminateRead() {}
function onTerminateWrite() {}
function writePipeWrite(...args) {}
function writePipeCork() {}
function writePipeUncork() {}
function terminateRead() {}
function terminateWrite() {}
function terminate() {}
function toErrorObj(err) {}
function toNativeError(obj) {}
function writeJson(data) {}
const queue = asyncQueue(({ id, data }, taskCallback) => {}, PARALLEL_JOBS);
function dispose() {}
function onMessage(message) {}
// 入口方法
function readNextMessage() {}
// 调用入口方法,开始从主进程读取消息(父进程写入到管道中的数据)
readNextMessage();
3.1 进程通信之子进程部分
结合父进程执行 childProcess.spawn(... { stdio: [, , , , 3, 4] }) 创建子进程时指定的文件描述符 3/4 作为父子进程间通信的自定义管道,子进程需要创建指定文件描述为 3的可写流(父进程的 eadPipe)文件描述为 4 的可读流(父进程的 writePipe)。
结合下面的代码你会发现一个有趣的事情,子进程的 writePipe 是父进程的 readPipe,而子进程的 readPipe 是父进程的 writePipe。
// 父进程从子进程的 stdio 获取通信管道
// 数组索引位置对应文件描述符 fd
const [, , , readPipe, writePipe] = this.worker.stdio
这是一种 Node.js 处理进程间通信的方式,子进程向 fd 为 3 的流写入,父进程读取,父进程向 fd 为 4 的流写入,子进程读取,这就能实现父子进程间的通信。
这类似某种接头方式:双方前往约定地点(指定fd),组织的人(父进程)发出(写入)暗号(数据),接头(子进程)人接收(读取)暗号(数据)。接头人(子进程)报出(写入)另一段暗号(数据),组织的人(父进程)核对(读取)这段暗号(数据)。。。。
3.2 监听事件
writePipe/readPipe 的相关结束、关闭、错误事件,事件 handler 是 onTerminatWrite/onError 方法;
3.3 获取并发数
从子进程的进程参数获取并发数量:process.argv 就是进程参数,如果你不记得了,去前文看看吧。这个数字也是在 childProcess.spawn('/usr/bin/node', ['dist/worker.js', '20']) 调用中传递的,20 就是并发数了;
3.4 声明标识符terminated
声明标识符 terminated 标识符,标识进程是否终止;
3.5 声明 nextQuestionId
声明 nextQuestionId 这个问询 id,后面用一次就会累加 1。这个 id 是子进程有些工作需要交给父进程查询时要用用到,主要是调用一些 loaderContext 或者 webpack api 时候要用。你是不是很好奇为啥子进程不自己调用?这是因为进程间通信传递的是序列化后的 JSON 数据,即便是正则都会丢失,所以方法不能全部传过来,所以只能穿数据,方法还是留给父进程,子进程如果需要的时候让父进程调用,父进程得到结果后再通过管道发送给子进程;
3.6 声明 callbackMap 常量
存储回调函数,key 就是上面的 nextQuestionId;
3.7 声明一堆方法
接着就是声明一堆方法,包含入口方法 readNextMessage(呵!这文章写的真简(偷)洁(懒),这些方法现在说也没用,用到一个讲一个,然后再梳理调用栈)
3.8 调用入口方法
入口方法 readNextMessage 被调用,开启子进程打工人的职业生涯。
四、总结
本篇小作文接前文的主进程后续,开始了子进程 worker.js 的代码详解,主要讨论了以下内容:
- 父进程创建子进程的过程分析,包含:
- 1.1 解析
node路径; - 1.2 分析
thread-loader下的worker.js路径; - 1.3 传递并发数配置;
- 1.3 设置
stdio:[, , , pipe, pipe]自定义管道的方式处理进程间通信;
- 1.1 解析
- 分析了
worker.js的文件结构,期间分析了自定义管道在子进程中的创建fd为3/4的流负责通信,得出readNextMessage这个入口方法的声明和调用;
下一篇我们真是进入到 readNextMessage 方法的细节,前方高能,请好好阅读已有的文章啊!
感谢阅读,你的点赞是我更文最大的动力!