多进程打包:thread-loader 源码(4)

183 阅读5分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的9天,点击查看活动详情

一、前情回顾

上文介绍 thread-loader 的入口方法 WorkerPool.prototype.run 方法的内部逻辑,重点介绍了 this.poolQueue.push 方法从接收数据(data, cb)一直到 neo-async 调用 worker 启动对 data 的处理的全过程。

这个部分涉及到 neo-asyncqueue 这个异步队列的逻辑,他是实现并发的核心,但有几点仍需要注意:

  1. workerPool.run(data, cb) 传递的第一个参数我们称之为 data 对象,第二个参数是回调,用于接收 worker 处理 data 后返回的 loaders 的执行结果的回调函数,称为 cb

  2. this.poolQueue 是调用 asyncQueue 方法得到的异步队列,声明该队列时传入的 workerthis.distribute 方法;

  3. neo-async 调用 worker 时, 即调用 this.distriburteworker 接收的第二个参数不是前面的 cb,而是 neo-async 内部的 runQueue 方法创造出来的 done 方法;

本篇小作文接上文讨论 wokerPool.runpush(data, cb) 后,neo-async 开始调用 woker 处理 data 的逻辑;

二、 this.distributeJob.bind(this)

通过前面的描述,我们得知 this.distributeJob 方法是 poolQueueworker,也就是负责处理 pushpoolQueuedata 的方法。distributeJob 方法是 WorkerPool 的原型方法;

export default class WorkerPool {
  constructor(options) {
    // ...
    this.poolQueue = asyncQueue(
      this.distributeJob.bind (this), // 创建 worker 并分配任务
      options.poolParallelJobs // 并发数
    );
    // ....
  }
  
  // 这个方法就是上面构造函数中创建 poolQueue 时传递的 worker
  distributeJob(data, callback) {}
}

2.1 this.distributeJob 方法细节

  1. 方法位置:thread-loader/src/WorkerPool.js -> WorkerPool.prototype.distributeJob

  2. 方法参数:

    • 2.1 data: 需要被 distribute 方法处理的 data 对象。这个 data 就是前文 workerPool.run(data, cb) 传入的包含 loadersdata 对象;
    • 2.2 callback: 回调函数,当 distributeJob 完成其既有逻辑后需要调用的回调。前面强调过这个 callback 的作用是接收 data 处理后的结果的,所谓处理结果就是 thread-loader 执行完 data 中的 loader 后得到的结果。注意,这个 callback 不是 workerPool.run(data, cb) 中传入的 cb,而是 neo-async runQueue 创建的 done 方法;
  3. 方法作用:distributeJob 顾名思义,分配任务。前面也提到过 WorkerPool 这个类就是负责调度 worker 的。具体逻辑如下:

    • 3.1 首先选取最合适的 worker,所谓最合适的标准是任务最少;
    • 3.2 判断找到的最合适的 worker 任务数是否为 0 或者当前 worker 池(this.workers)中 worker 数量是不是已经超过了 numberOfWorkers 的数量限制,如果超过了就用找到的工作数量最少的 worker 处理任务。
    • 3.3 这里相当于是 this.workers 中的 worker 数量尚未超出哦 numberOfWorkers 数量限制的情况,这个时候新建 worker 来运行任务;
  4. workerWorkerPool 实例的关系

这里先解析一个名词:worker;在 thread-loader 中到处都是 worker,这让个初看的人十分懵逼,另外我觉得这个命名方式也是扯淡极了,虽然作者设计的很🐂🍺,但是这个槽点也必须不能放过他。

第一个 worker 就是 this.poolQueueworkerpoolQueue 的 worker 说的是 this.distributeJob 这个方法。这个 worker 是处理 data 的一个方法。

另外还有一个东西也叫做 worker,下面讲 createWorker 的时候就会提到。在 createWorker 中创建的 WorkerPool 实例也叫做 worker,但是这个 worker 和上面的 poolQueue worker 不一样,这里的 worker 是个实实在在的 子进程

workerWorkerPool 实例的关系,类比 快递快递员 的关系,而 WorkerPool 类则相当于快递公司;

worker 承载具体任务,而 workerPool 则负责调度管理这些 worker,就像网购买的东西发的是 快递包裹,而不是发一个 快递员,但是最后由 快递员 配送。具体逻辑如下:

export default class WorkerPool {
  constructor(options) {
    // ...
    this.poolQueue = asyncQueue(
      this.distributeJob.bind (this), // 创建 worker 并分配任务
      options.poolParallelJobs // 并发数
    );
    // ....
  }


  distributeJob(data, callback) {
  
    // 找到任务最少的 worker 并使用他
    let bestWorker;
    for (const worker of this.workers) {
      if (!bestWorker || worker.activeJobs < bestWorker.activeJobs) {
        bestWorker = worker;
      }
    }
    if (
      bestWorker &&
      (bestWorker.activeJobs === 0 || this.workers.size >= this.numberOfWorkers)
    ) {
      // bestWorker.activeJobs 是说当前 worker 没有任务
      // this.workers.size > this.numberOfWorkers 时,
      // 说明已有的 worker 已经超过 numberOfWorkers 的数量限制了,
      // 不能再让后面的新建 workers 了,说明找遍了所有的 worker 它就是最好的了
      bestWorker.run(data, callback);
      return;
    }

    // 如果走到这里,说明一个 worker 也没有,
    // 或者此时 workers 数量没有超出 this.numberOfWorkers 的限制
    // 此时新建 worker 然后调用 worker 的 run 方法
    const newWorker = this.createWorker();

    // 得到新的 newWorker 后,调用 newWorker.run 传入 data 
    // 和 distributeJob 的 callback 回调 callback
    // callback 是 runQueue 创造的 done 方法
    newWorker.run(data, callback);
  }

  createWorker() {}
}

2.2 this.createWorker 方法

  1. 方法位置:thread-loader/src/WorkerPool.js -> WorkerPool.prototype.createWorker

  2. 方法参数:暂无

  3. 方法作用:创建一个 PoolWorker 实例,注意看好了,是 POOL-WORKER 而不是 WORKER-POOL,单次换了换位置。。。很让初看的人费解。另外下面也要称呼 PoolWorker 的实例为 worker 了,上面已经区分过了,这个 worker 是子进程。具体工作如下:

    • 3.1 传入配置 workerNodeArgsparallelJobs 创建 PoolWorker 实例,其中 workerNodeArgs 表示传递给子进程的进程参数(比如执行命令:$ node some-cmd --evn--evn 就叫做进程参数);this.onJobDoneworker 完成任务时需要触发的回调,这个就好像你上完一天班后下班打卡一样。
    • 3.2 parallelJobs 这表示可以并发执行的任务数量限制
    • 3.3 把新建的 PoolWorker 实例放到 this.workers 池子中,方便找到任务数最少的 worker,也方便统计现有 worker 数量,防止超出 this.numberOfWorkers 的数量限制;
export default class WorkerPool {
  constructor(options) {}

  distributeJob(data, callback) {
    const newWorker = this.createWorker();

    newWorker.run(data, callback);
  }

  createWorker() {
    // 创建一个新 worker 实例
    const newWorker = new PoolWorker(
      {
        nodeArgs: this.workerNodeArgs,
        parallelJobs: this.workerParallelJobs,
      },
      () => this.onJobDone() // worker 的任务完成时要触发的回调
    );
    
    // 把 worker 实例放到 worker 池子中
    this.workers.add(newWorker);
    
    // 返回新建 worker
    return newWorker;
  }
 
  onJobDone() {  }
}

三、总结

本篇小作文讨论了 workerPool.poolQueueworker 方法, WorkerPool.distributeJob 方法的详细逻辑:

  1. 找到最合适的 worker(子进程),标准是活跃任务数最少的;
  2. 如果没有超过 this.numberOfWorkers 的限制就新建 workerworkerPoolWorker 的实例,可以理解成子进程;

受限于篇幅,下一篇我们详细讲解 PoolWorker 这个类是如何创建子进程以及父子进程间的通信。