持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的24天,点击查看活动详情
码字不易,感谢阅读,你的点赞让人振奋!
如文章有误请移步评论区告知,万分感谢!
未经授权不得转载!
一、前情回顾
上一篇小作文准备把由 child_process 实现的 thread-loader 变成 worker_threads 的 thread-loader,这才是名副其实的多线程(multi-threads),主要盘点了以下内容:
- 回顾 thread-loader 的工作原理;
- 简单介绍进程、线程区别和联系;
- 简单了解 worker_threads 模块的能力;
- 分析了 worker_threads 在 js 模块执行和通信方面满足 thread-loader 的要求;
从本篇开始,我们手把手的改造 thread-loader 代码,跟上步伐开始吧~
二、src/WorkerPool.js
PoolWorker 是用于创建 worker 的类,在 worker_threads 层面的改造共分以下三种情况:
- worker 有 child_process.spawn 创建的子进程改为 worker_threads 创建的线程;
- 修改 child_process 的自定义管道通信方式为子线程 postMessage 方式;
- 修改处理收到来自子进程的消息后获取结果回调的方式;
2.1 修改 worker 创建方式
- 原有的 child_process.spawn 创建子进程 worker
childProcess.spawn 方法来自 Node.js 的 child_process 模块,用于衍生子进程,其接收的参数及作用如下:
- 1. 第一个参数是要执行的命令,process.execPath 是你的机器上的 node 可执行文件路径;
- 2. 第二个参数是传递个要执行的命令的参数,workerPath 是 dist/worker.js;
- 3. 第三个参数,是配置子进程行为的,例如下面的 detached 表示脱离父进程独立运行,stdio 是配置子进程 stdin,stdout,stderr 以及自定义管道的;
class PoolWorker {
constructor(options, onJobDone) {
// Empty or invalid node args would break the child process
const sanitizedNodeArgs = (options.nodeArgs || []).filter((opt) => !!opt);
// 子进程的方式创建 worker
this.worker = childProcess.spawn(
process.execPath,
[].concat(sanitizedNodeArgs).concat(workerPath, options.parallelJobs),
{
detached: true,
stdio: ['ignore', 'pipe', 'pipe', 'pipe', 'pipe']
}
);
}
}
- 改为 worker 创建子线程 worker
- 2.1 导入 worker_threads 模块,并结构出 Worker 构造函数;
- 2.2 在 PoolWorker 类的构造函数中初始化 Worker 实例,该实例以为一个线程,在实例化时传入 workerPath,即 dist/worker.js 这个文件;argv 对应的是线程的进程参数,这里我们只传入了一个并发任务数,原则上也可以吧 sanitizedNodeArgs 也传入;
- 2.3 调用 worker.unref 方法,该方法可以让线程独立运行;
import { Worker } from 'worker_threads';
class PoolWorker {
constructor(options, onJobDone) {
const sanitizedNodeArgs = (options.nodeArgs || []).filter((opt) => !!opt);
// 使用 worker_threads 下的 Worker 创建 woker 线程
this.worker = new Worker(workerPath, {
argv: [options.parallelJobs]
})
this.worker.unref();
this.readNextMessage();
}
}
经过上面的操作,把原来的子进程变成了子线程,据传创建线程的开销比创建进程的小,有希望改善一下性能,现在大有希望 YY 一波了~~~
2.2 修改通信方式
关于通信方式估计是要费点笔墨的,为了让前面没有看过 thread-loader 源码系列的看官老爷们也能看得懂,我觉得下一篇专门写一篇这个通信方式修改的文章。说句实话,当时看 thread-loader 源码的时候最让我不理解的就是这个进程间通信,着实让我耗掉了不少头发!
2.3 获取结果回调
上面两个主题已经介绍了如何把进程改为线程、通信方式的变更,这第三部分相当于是第二个主题的进阶改造。
因为通信方式的改造,之前通信时的数据格式和协议已经发生了巨大变化。着这些通信方式中,最主要的是运行于 worker(子进程或线程)中 loader 在完成其工作后,需要把 loader 的运行结果发送会主进程。
在此之前,为了保证没有读过前面源码的看官老爷能够看懂,我先需要先简单介绍一下原发送方式,再介绍新的发送方式;
- 基于自定义管道的发送方式
首先是在 loader 运行的结果的回调函数中获取 loader 运行结果中的这个中依赖:fileDependency/contextDependency/missingDependency,接着对这些依赖进行加工,为啥要加工呢?
是为了适配基于自定义管道的发送方式,这些依赖中有的是字符串、有的是 buffer,直接发送会被 JSON.stringify 序列化丢失掉一些数据,因此需要把这些数据都搞成另一个 bufferToSend ,然后把这些依赖的描述信息比如长度、是否是字符串等信息作为需要序列化的信息发生给父进程。
待到父进程接收到了描述信息之后,再按照描述信息读取 bufferToSend 中的二进制数据还原成数据原本的样子。
代码如下:
loaderRunner.runLoaders(
{
loaders: data.loaders,
resource: data.resource,
readResource: fs.readFile.bind(fs),
context: {}, // 构造一个 loaderContext 对象,其中模拟了很多原 loaderContext 对象上的方法
},
(err, lrResult) => {
// loader 结果回调
const {
result,
cacheable,
fileDependencies,
contextDependencies,
missingDependencies,
} = lrResult;
const buffersToSend = [];
const convertedResult =
Array.isArray(result) &&
result.map((item) => {
const isBuffer = Buffer.isBuffer(item);
return {
data: item,
};
});
writeJson({
type: 'job',
id,
error: err && toErrorObj(err),
result: {
result: convertedResult,
cacheable,
fileDependencies,
contextDependencies,
missingDependencies,
buildDependencies,
},
data: buffersToSend.map((buffer) => buffer.length),
});
buffersToSend.forEach((buffer) => {
writePipeWrite(buffer);
});
setImmediate(taskCallback);
}
);
} catch (e) {
}
}, PARALLEL_JOBS);
- 基于 message 事件和 postMessage 方法的新通信方式的发送
上一步已经说的很请求了,为啥自定义管道通信需要经过复杂的转换才能把结果发送过去,主要还是因为自定义管道本质上发送的 JSON.stringify 序列化出来的字符串,具体的数据如果直接序列化就会丢失,别说二进制的数据,就是正则直接序列化都会丢失😂。
而 message 事件和 postMessage 就方便多了,它除了可以发 JSON 字符串,还可以发送 Javascript 对象,不过也不是发送引用地址,而是经过了一个克隆算法,简单来说就是发送了一个对象过去。所以我们就可以废弃调用在结果回调中的转换过程:
loaderRunner.runLoaders(
{
loaders: data.loaders,
resource: data.resource,
readResource: fs.readFile.bind(fs),
context: {},
},
(err, lrResult) => {
writeJson({
type: 'job',
id,
error: err && toErrorObj(err),
result: lrResult
});
setImmediate(taskCallback);
}
);
注意:你会发现我们改造的通信方式中还是使用的 JSON.stringify 序列化的,这么做是为了偷懒,虽然 postMessage 可以发送对象,但是面对一些循环引用还是不行。这么做就会导致有些返回二进制的 loader 会卡住不动了。
三、总结
从这篇小左开始,我们已经开始动手该改造 thread-loader 了,这个改造目前主要分为以下三方面:
- 改用 worker_threads 创建 Worker 线程的 woker;
- 将原有的自定义管道的通信方式改为 message 事件和 postMessage 方法的方式;
- 将原有的对 loader 运行结果的改造的部分代码移除,直接发送 loader 运行结果;
其中第二点在下一篇小作文用一个个专门的篇幅讲述!