Node实现并行任务执行控制

211 阅读3分钟

Node有很多优秀的流程控制库,比如asyncbluebird等。但是为了学习,我们不使用这些优秀的第三方库,而是我们自己来思考如何实现并行任务的控制。

在Node中,如果我们要并行执行多个任务,可以使用Promise.all来实现,如下面伪代码:

const result = await Promise.all([
    readFileAync, // 异步读取文件
    countDocument, // 统计数据库文档数
    loadRemoteConfig, // 加载远程配置
]);

上面的写法中,虽然任务都是同时触发的,但是无法限制同一时刻触发的任务数。如果是大量读取数据库的操作,不加以限制可能会对数据库服务造成影响。

因此,我们需要想办法去限制并发数。那该如何去限制呢?

我们可以用最大并行数当前进行中的任务数两个变量,来控制同一时刻并行运行的最大任务数。具体实现的步骤如下:

  1. 设置任务最大并行数,并将当前进行中的任务数初始化为零;
  2. 判断当前进行中的任务数是否小于最大并行数,如果满足条件则立即执行任务,并将当前进行中的任务数加一。否则,就将新的任务入队等待执行;
  3. 任务执行完成后,需要将当前进行中的任务数减一,以便队列中的任务可以正常执行;
  4. 一旦当前进行中的任务数小于最大并行数,就从队列中取出任务执行,直到队列为空。

image.png

在理解完上面的执行逻辑后,我们正式进入编码环节。

首先,我们需要定义上文中所指的两个控制变量,以及一个用来存放待执行任务的队列。因此,我们可以定义这样一个TQ类:

class TQ {
  queue; // 待执行任务队列
  count; // 当前进行中的任务计数
  concurrency; // 并行数

  constructor(concurrency) {
    this.queue = [];
    this.count = 0;
    this.concurrency = concurrency || 3;
  }
}

接着我们还需要定义入队/出队方法:

  // 入队
  enqueue(handler) {
    if (typeof handler !== 'function') {
      throw new Error('"handler" must be a function object')
    }

    return new Promise((resolve, reject) => {
      this.queue.push({
        handler, resolve, reject,
      });
    });
  }

  // 出队
  dequeue() {
    if (this.count > this.concurrency || this.queue.length <= 0) {
      return 0;
    }

    const {
      handler, resolve, reject,
    } = this.queue.shift();

    this.run(handler)
      .then(resolve)
      .catch(reject);

    return 1;
  }

值得注意的是,入队方法enqueue返回的是一个Promise对象,只有在任务执行完成(成功或者失败)时才返回。如果只返回一个普通对象,那么就无法获取任务的执行结果了。

在执行任务前后,需要同步维护当前进行中的任务计数的值。因此,我们可以定义一个任务执行器方法:

  // 任务执行器
  async run(handler) {
    this.count += 1;

    const reply = await handler();

    this.count -= 1;

    this.dequeue();

    return reply;
  }

每次在添加新任务时,要根据当前进行中的任务数判断是立即执行任务,还是放入队列中等待执行,所以还需要一个任务调度器方法:

  // 任务调度器
  async dispatch(handler) {
    if (this.count < this.concurrency) {
      return this.run(handler);
    }

    return this.enqueue(handler);
  }

最后,通过Promise.all方法将任务列表逐一通过dispatch方法调度执行,就可以并行的执行任务且一次至多执行N个。

完整的示例代码可以参考:传送门