多进程打包:用 worker_threads 改写 thread-loader(1)
码字不易,感谢阅读,你的点赞让人振奋!如文章有误请移步评论区告知,万分感谢!未经授权不得转载!
一、前情回顾
上一篇小作文是 thread-loader 源码部分的最后一篇,现在回顾一下 thread-loader 的整个工作流程:
- thread-loader 的 pitch 方法拦截它后面的所有 loader;
- 创建 WorkerPool 实例 workerPool,它是个进程池子,用以调度进程;调度工作依赖使用 neo-async/queue.js 创建的 poolQueue 队列;
- poolQueue.push(data, callback);
- poolQueue.push 后会执行 poolQueue 的 worker 函数 —— distributeJob 创建子进程;
- distributeJob 创建子进程,通过自定义管道通信,利用 readPipe 接收子进程消息,利用 writePipe 向子进程发送消息,通信的数据载体是 JSON 格式字符串;
- 子进程接收来自父进程发送过来的消息运行 loader,碍于进程间通信限制,子进程自己构造了一个 loaderContext 对象,当用到父进程 loaderContext 中的方法时,构造的 loaderContext 对象会通过进程间通信委托父进程实现;
- 当子进程完成 runLoaders 工作后,在回调中利用管道向父进程发送结果;
- 父进程收到消息后,找到本次运行 loader 时对应的回调函数,在回调函数中把这些结果 —— 各种类型的依赖,添加到构建中;
我们之所以要这么细致的阅读这个 loader 的源码,是因为我的任务除了在框架层面接入 thread-loader 之外还需要尝试用多线程改写 thread-loader。
经过源码部分可知:
- thread-loader 虽然叫 thread loader,但实现确实名不副实的 child_process 即子进程;
- 系统开一个子进程的开销比新开一个线程(worker_threads)大的多;
基于上述内容,我们准备用多线程实现一个多线程打包。这个任务最终效果如何,没有人知道,总之是个探索性的任务!
给年轻人一个忠告:不建议认领这种尝试性的 KPI 工作,很可能最后没有产出,只有个行不通的结论!
二、worker_threads —— 工作线程
既然准备用要用多线程,先来认识一下我们后半场的主角 worker_threads
2.1 进程 vs 线程
- 进程 进程是操作系统进行资源分配最小单位,同一时间内,同时执行的进程不会超过 CPU 的核心数(这个就是大家电脑上的4核、6核、12核)。可以粗陋的理解一个程序运行运行就是一个进程,每个进程都拥有单独的硬件资源,之所以这么设计,是为了满足切换程序时可以快速的回到刚刚的状态。
比如你的电脑上聊天(微X)同时看视频(爱P艺),这就是两个程序,是两个进程。
- 线程
线程隶属于某个进程,线程是程序执行过程的最小单元,他们共享进程的硬件资源。一个程序内包含多种任务,还是拿视频播放为例,视频播放时有图像、有音频两种任务,为了保证音画同步,就需要他俩同时开工。否则,当要拖动进度条时就会出现音画不同步的诡异场景。
- 主要区别:
线程和进程的最主要区别:进程间天然隔离,他们的分配到的硬件资源是隔离的,而线程间是贡献它所属进程的资源;
2.2 Node.js 中的 worker_threads 模块
worker_threads
模块用于创建并执行 JavaScript 的线程,用法:
const worker = require('worker_threads');
2.2.1 举个例子
- wt.js
const {
Worker,
isMainThread,
parentPort,
workerData
} = require('worker_threads');
if (isMainThread) {
module.exports = function parseJSAsync (script) {
return new Promise((resolve, reject) => {
console.log(__filename);
const worker = new Worker(__filename, {
workerData: script
});
worker.on('message', resolve);
worker.on('error', reject);
worker.on('exit', code => {
if (code !== 0) {
reject(new Error('worker stopped with exit code ' + code))
}
});
})
}
} else {
const { parse } = require('./sub');
const script = workerData;
parentPort.postMessage(parse(script))
}
- ./sub.js
console.log('hahahhahahahah');
exports.parse = (s) => {
console.log('sub parse')
console.log(s);
return 'sub - parse'
}
2.2.2 worker_threads 的一些属性作用
-
Worker, 主线程用于创建 worker 线程的对象类型,包含 MessagePort 操作以及一些特有的子线程元数据( meta data)
-
isMainThread, false 标识当前代码作为子线程运行,true 则表示主线程
-
parentPort, 在 worker 线程里表示父进程的 MessagePort 类型的对象,在主线里为 null,这个 parentPort 是用于父子通信的;
-
workerData, 在 worker 线程里是父进程创建 worker 线程实例时初始化的数据,在主线程中为 undefined;
-
threadId, 当前 worker 线程的线程 ID,在父进程里是 0;
-
MessageChannel, 包含一对儿能够互相跨线程通信的 MessagePort 类型的对象,可用于创建自定义的通信频道,用于线程间通信;
-
MessagePort, 用于线程间通信的句柄,继承自 EventEmitter 类,有 close 等事件;
三、改写 thread-loader 需要 worker_threads 哪些能力?
在改写之前我们首先要斟酌哪些能力是必须具备的,这些就是需要前期调研的工作,如果这些必要能力都具备在开工,否则容易干到一半儿才发现有致命缺陷导致这条路走不通,这就是磨刀不费砍柴工!关于对 woker_threads 的要求能力如下:
3.1 创建子线程执行 js 模块
对应 thread-loader 中调用 child_process.spawn 启用新的进程运行 js 模块,这就要求 worker_threads 也必须可以创建线程并且无限制能力的执行 js 模块;
通过上面的例子可以发现,new worker_threads.Worker() 对应了这一能力!
const { Worker } = require('worker_threads');
new Worker('dist/worker.js');
3.2 通信
这方面的要求主要来自两方面:
-
对应 thread-loader 运行于子进程的 loader 在执行结束之后需要把 loader 的结果给到主进程,以便继续 webpack 的打包工作流;
-
运行在子进程的 loader 并没有获取原来的 loaderContext 的能力,子进程有些工作需要委托给主进程完成,待完成后再将结果发送给子进程;
thread-loader 使用 child_process 时的解决方案为自定义管道完成 IPC(进程间通信),stdio 的第四个、第五个 'pipe' 便是文件描述符为 3、4 的自定义管道,一个用于发送,一个用于读取,如下:
improt child_process from 'child_process';
const worker = child_process.spawn('node', 'dist/worker.js', {
stdio: ['pipe', 'pipe', 'pipe', 'pipe', 'pipe']
});
// 自定义管道
cosnt [,,, readPipe, writePipe] = worker.stdio;
这期间并不需要子线程与子线程间通信,只需要子线程与主线程进行通信,worker_threads 对应的解决方案为:worker.postMessage & parentPort.postMessage;
在子线程中访问 parentPort,通过他把内容发送给主线程。在主线程中通过 worker.postMessage 即可把内容发送给子线程;
四、总结
本篇小作文开始上手鼓捣 thread-loader,准备把由 child_process 实现的 thread-loader 变成 worker_threads 的 thread-loader,这才是名副其实的多线程(multi-threads),主要盘点了以下内容:
- 回顾 thread-loader 的工作原理;
- 简单介绍进程、线程区别和联系;
- 简单了解 worker_threads 模块的能力;
- 分析了 worker_threads 在 js 模块执行和通信方面满足 thread-loader 的要求;
从下一篇开始,我们手把手的改写这代码,来吧加入我们!
转载自:https://juejin.cn/post/7110039728488972324