持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的11天,点击查看活动详情
一、前情回顾
上一篇小作文接着 workerPool.run(data, cb) 内部通过调用创建 poolQueue 传递的 worker 方法即 this.distributeJob 方法处理 data 的位置继续讨论了:
-
this.distributeJob方法寻找最合适的worker或者创建worker处理data,创建worker就来到了PoolWorker构造函数; -
PoolWorker构造函数主要作用是通过child_process.spawn创建子进程运行thread-loader/dist/worker.js这个文件处理data,将spawn返回的子进程对象挂载到PoolWorker实例的worker属性上,即this.worker。 -
在创建子进程的时候通过传递给
spawn的options.stdio自定义管道实现进程间通信,这里使用的自定义管道是Stream对象。这些自定义的管道可以通过子进程对象.stdio[索引]的方式访问到,即this.worker.stdio[索引],这里的索引0,1,2分别是标准输入、标准输出、标准错误,3及以后的是自定义管道。
本文接上文 PoolWorker 构造函数将自定义管道挂载到 PoolWorker 实例——this.readPipe/wirtePipe 挂载完成后的内容。
二、PoolWorker.prototype.listenStdOutAndErrFromWorker
-
方法位置:
thread-loader/src/WorkerPool.js -> PoolWorker.prototype.listenStdOutAndErrFromWorker -
方法参数:
- 2.1
workerStdout:子进程的stdout - 2.2
workerStderr:子进程的stderr
- 2.1
-
方法作用:监听子进程的
stdout/stderr的data事件,把收到的输入输出到父进程的标准输出、标准错误
class PoolWorker {
constructor(options, onJobDone) {
// 监听子进程的 stdout 和 stderr 的 data 事件
// 把 stdout 和 stderr 输出的内容输出到父进程的 shell
this.listenStdOutAndErrFromWorker(this.worker.stdout, this.worker.stderr);
}
}
listenStdOutAndErrFromWorker(workerStdout, workerStderr) {
if (workerStdout) {
workerStdout.on('data', this.writeToStdout);
}
if (workerStderr) {
workerStderr.on('data', this.writeToStderr);
}
}
// 写入父进程的 stdout
writeToStdout(data) {
if (!this.disposed) {
process.stdout.write(data);
}
}
// 写入父进程的 stderr
writeToStderr(data) {
if (!this.disposed) {
process.stderr.write(data);
}
}
三、PoolWorker.prototype.readNextMessage
在执行 PoolWorker 的构造函数中被调用;
-
方法位置:
thread-loader/src/WorkerPool.js -> PoolWorker.prototype.readNextMessage -
方法参数:暂无
-
方法作用:
- 3.1 更新
this.state值为'read length'表示开始读取本次message的长度,这个是buffer长度; - 3.2 调用
this.readBuffer方法先读取长度,传入4,标识4个字节,因为存储message长度的是一个32位整数,从其回调的readIn32BE方法调用也可佐证这一点。
- 3.1 更新
class PoolWorker {
constructor(options, onJobDone) {
// 读取消息,消息哪里来的呢?
// 很显然是子进程抛过来的,至于抛出消息这是后话了
this.readNextMessage();
}
// 读取消息
readNextMessage() {
this.state = 'read length';
// 读取长度
this.readBuffer(4, (lengthReadError, lengthBuffer) => {
// ... 读取长度后的操作
});
});
}
}
3.1 PoolWorker.prototype.readBuffer
-
方法位置:
thread-loader/src/WorkerPool.js -> PoolWorker.prototype.readBuffer -
方法参数:
- 2.1
length: 要读取的buffer长度 - 2.2
callback: 取出完成后要执行的回调函数
- 2.1
-
方法作用:科里化
readBuffer方法,绑定可读流对象为this.readPipe,从前文可知,这个readPipe是父子进程通信的管道。事实上,readPipe是一个可读流对象,由thread-loader下的worker.js文件在子进程中运行时创建。
class PoolWorker {
readBuffer(length, callback) {
// 科里化 readBuffer 绑定 readPipe
readBuffer(this.readPipe, length, callback);
}
}
3.2 readBuffer 方法
-
方法位置:
thread-loader/src/readBuffer.js -> function readBuffer -
方法参数:
- 2.1
pipe:数据来源管道对象,在thread-loader中就是用于父子进程通信的this.readPipe这个管道 - 2.2
length:本次要读取的长度,是buffer长度;你有没有想过,我怎么知道要读取多长?每次放回的结果长度肯定不一致的?记好这个问题,后面会有答案的; - 2.3
callback:读取成功后要执行的回调含糊
- 2.1
-
方法作用:下面的代码时经过简化过得,结构很清晰
- 3.1 判断如果
length为0,则直接分配一个0字节的buffer,然后调用callback,因为不需要读取; - 3.2 调用
readChunk私有方法,这个方法内部实现就是给pipe绑定data事件,给可读流绑定data事件会触发缓存中的数据转移出来,这样就能接收到缓存区的数据了,把接收到的数据交给onChunk回调处理
- 3.1 判断如果
export default function readBuffer(pipe, length, callback) {
if (length === 0) {
callback(null, Buffer.alloc(0));
return;
}
let remainingLength = length;
const buffers = [];
const readChunk = () => {
const onChunk = () => { /* 具体读取 buffer 数据的逻辑 */ };
pipe.on('data', onChunk);
pipe.resume();
};
// 调用 readChunk
readChunk();
}
3.3 onChunk 回调
-
方法位置:
thread-loader/src/readBuffer.js → function readBuffer → readChunk → onChunk -
方法参数:
arg,从可读流对象得到的数据块 -
方法作用:从
pipe缓存的buffer读取指定长度的内容,如果超出长度把超过的内容退回pipe的缓存区;详细如下:- 3.1 设置
overflow,用以承载超过readBuffer指定的length长度的数据,以备后面退回缓冲区 - 3.2 判断
chunk.length > reamainingLength,remainingLmength是readBuffer接收到参数lenght,表示要读取多长的buffer。如果chunk.lenght大于remaingLength,说明超了,这个时候就直接读取remaingLength长度,剩下的复制到到overflow。否则说明读取的不超过remainingLenght,读取然后扣除已经读取的长度,重新计算剩余可读取长度。 - 3.3 把本次读取的
buffer和之前的读取的buffer拼接;之所以要和之前的拼接,是因为读取 buffer 这个事儿可能无法一次性读取够,每次onChunk触发都不再有之前的数据,所以要自己把前面已经读取过的保存好。 - 3.4 当
remainingLength为0,说明已经读取够了readBuffer指定的lenght长度,此时- 3.4.1 移除
pipe的data监听器,这么做可以让pipe暂停转移缓冲区的数据出来; - 3.4.2 调用
pipe.pause()也是暂缓转移缓冲区数据; - 3.4.3 如果有
overflow说明有超过length的数据,调用pipe.unshift退回pipe的缓冲区; - 3.4.4 调用
readBuffer的callback并传入长度为length的buffer数据;
- 3.4.1 移除
- 3.1 设置
// readBuffer 接收到指定长度,
// 表示还可以再读取多少长度的 buffer
let reaminingLength = length;
const buffers = [];
const readChunk = () => {
// ...
// 接收 pipe 缓冲的 buffer
const onChunk = (arg) => {
let chunk = arg;
// 设置 是否超出标识符
let overflow;
// chunk.length > reaminingLnegth 说明超过指定长度了
if (chunk.length > remainingLength) {
// 把超超出长度的 buffer 复制出来
overflow = chunk.slice(remainingLength);
// 复制 remaingingLength 长度的的 buffer
chunk = chunk.slice(0, remainingLength);
// 上面两步已经复制 reaminingLength,说明第一次就读取够 readBuffer 指定的 length 长度
remainingLength = 0;
} else {
// 说明不够 remainingLength
remainingLength -= chunk.length;
}
buffers.push(chunk);
if (remainingLength === 0) {
pipe.removeListener('data', onChunk);
pipe.pause();
if (overflow) {
pipe.unshift(overflow);
}
callback(null, Buffer.concat(buffers, length));
}
};
pipe.on('data', onChunk);
pipe.resume();
};
3.4 this.readNextMessage 读取长度后的回调
前面我们分析了 readBuffer 的工作,它会从 readPipe 中读取指定长度的 buffer 数据,然后调用回调;现在我们看看在 this.readNextMessage 中调用 this.readBuffer 后的回调中的工作;
- 更新
this.state为 长度读取成功 ——lenght read - lengthBuffer.readInt32BE 从指定的
offset处的bufer读取有符号的大端序32位整数,说人话就是子进程写入管道的数据总长度,告诉后面处理消息的方法要读取多少长度就能取到本次需要的数据; - 更新状态为
read message,表示已经知道子进程写入的数据(message所谓消息,是为了父子通信这个场景更应景儿的叫法,就是子进程跑完loader以后的数据) - 再次调用
this.readBuffer读取数据,并且传入上一次readBuffer得到的长度;最后在本次readBuffer回调中处理消息编码、转成JSON,最后触发this.onWorkerMessage方法处理消息;
class PoolWorker {
// 读取消息
readNextMessage() {
this.state = 'read length';
// 读取长度
this.readBuffer(4, (lengthReadError, lengthBuffer) => {
// 这个回调就是 readBuffer 后要执行的,以忽略错误处理
// 更新 this.state 为 length 读取完成
this.state = 'length read';
// 获取 length
const length = lengthBuffer.readInt32BE(0);
this.state = 'read message';
this.readBuffer(length, (messageError, messageBuffer) => {
// 处理读取数据的回调
// 后文专门开篇讨论
});
});
}
}
四、总结
本篇小作文详细讨论了 Pool.prototype.readNextMessage 的逻辑,它的核心就是先从进程通信管道 readPipe 中通过 this.readBuffer 先读取数据长度,读取到长度之后再调用 this.readBuffer 读取数据,最后交给处理数据的方法;
此外,我们还详细介绍了 readBuffer 方法的工作原理:监听 readPipe 的 data 事件,读取指定长度的 buffer;在进程通信中,通信的消息由两部分构成:数据长度 + 数据;这个设计很 nice,数据长度是个 32 位正数,已知 4 个字节长度。
所以每次先读取 4 个字节,得到后面的数据长度,这样就解决了每次读取数据不一样长的。不一样长好办啊,子进程会告诉我们数据长度,我们只需要获取一下这个长度就可以了。