面试官:请手写一个并发控制的函数

816 阅读4分钟

面试官:

请手写一个并发请求控制的函数/类,传入一个请求列表和最大并发数,这个函数可以控制请求的并发数,又可以实现并发,最后返回所有的请求结果

怎么用

先看结果,这个类怎么用

  • 在构建类的实例对象的时候传入最大并发数
  • 然后调用类的promiseAll方法去执行我们的请求列表,其余的表现形式等同于Promise.all
const plimit = new PLimit(3);
plimit
  .promiseAll([
    () => asyncFun("aaa", 2000),
    () => asyncFun("bb", 3000),
    () => asyncFun("ccc", 1000),
    () => asyncFun("ccc", 1000),
    () => asyncFun("ccc", 1000),
  ])
  .then((res) => {
    console.log(res);
  });

解析

首先构建一个类,用属性count来存储最大并发数,用queue控制并发调度的队列

class PLimit {
  count = 6; // 最大并发数
  readonly queue: Function[] = []; // 调度队列
  constructor(count: number) {
    // 数据初始化
    this.count = count;
  }
}

prmiseAll

然后在类里面写一个prmiseAll方法,这个是我们请求的入口函数。

  • 如果是以前直接使用原生的Promise.all的时候我们直接传入reqList给promiseAll就好,但是异步请求的状态改变我们没办法控制
  • 但是由于我们要实现并发数控制,所以我们要在请求外面再包裹上一层promise,我们可以自己手动控制这一层的promise什么时候resolve
private async enqueue(fn: Function, resolve: Function) {
    // 因为后面有对象, 所以保存本对象的this指向方便后面用
    // 入队,格式为含有fn和index的对象
    this.queue.push(this.run.bind(this, fn, resolve));
    // 判断是否达到最大并发数,没有的话继续出队执行函数
    if (this.activeCount < this.count && this.queue.length > 0) {
      const func = this.queue.shift();
      func && func();
    }
  }

enqueue

然后是入队函数**enqueue**

通过bind将请求函数fn等参数绑定在run函数()上,然后push到队列里面,

然后判断当前并发数,如果小于最大并发数的话,就可以继续执行队列里的请求函数

private async enqueue(fn: Function, resolve: Function) {
    this.queue.push(this.run.bind(this, fn, resolve));
    if (this.activeCount < this.count && this.queue.length > 0) {
      const func = this.queue.shift();
      func && func();
    }
  }

run

然后是执行方法run

  • 请求执行前activeCount++,执行结束activeCount—,并且把结果resolve给外层的promise,结束掉这一个请求
  • 然后继续出队,执行(有没有发现,其实本质是 队列调度+递归)

由于await不能捕捉错误,我本来还在外面包裹了try catch,后来一想如果请求出错,没有catch的错误会继续向外面抛出,最后会被原生的Promise.all包裹住的,所以不用担心

private async run(fn: Function, resolve: Function) {
    this.activeCount++;
    const res = await fn();
    resolve(res);
    this.activeCount--;
    if (this.queue.length > 0) {
      const func = this.queue.shift();
      func && func();
    }
  }

全部代码:

最后是全部的代码,加了点注释和细节

// 其实就是本质就是利用队列去调度异步请求的执行,
// 利用promise.all去获取所有请求的执行结果
class PLimit {
  count = 6; // 最大并发数
  activeCount = 0; //当前并发数
  readonly queue: Function[] = []; // 调度队列
  constructor(count: number) {
    if (!((Number.isInteger(count) || count === Infinity) && count > 0)) {
      throw new TypeError(
        "Expected `concurrency` to be a number from 1 and up"
      );
    }
    // 数据初始化
    this.count = count;
  }
  async promiseAll(reqList: Function[]) {
    // 使用promise.all的时候,自己用一个promise去包装整个(入队enqueue,执行run,然后await到结果的过程),
    // 等待到结果之后,调用resolve就能resolve掉这个promise
    return Promise.all(
      reqList.map(
        (fn, i) =>
          new Promise((resolve, reject) => this.enqueue(fn, resolve, reject))
      )
    );
  }
  /**
   * @description: 具体执行请求的函数
   * @param {number} index
   * @return {*}
   */
  private async run(fn: Function, resolve: Function, reject: Function) {
    this.activeCount++;
    try {
      const res = await fn();
      resolve(res);
    } catch (e) {
      reject(e);
    }
    this.activeCount--;
    if (this.queue.length > 0) {
      const func = this.queue.shift();
      func && func();
    }
  }
  /**
   * @description: 入队函数
   * @param {Function} fn 异步请求函数
   * @param {number} index 异步请求函数对应的index
   * @param {array} rest 剩余参数
   * @return {*}
   */
  private async enqueue(fn: Function, resolve: Function, reject: Function) {
    // 因为后面有对象, 所以保存本对象的this指向方便后面用
    // 入队,格式为含有fn和index的对象
    this.queue.push(this.run.bind(this, fn, resolve, reject));
    // 判断是否达到最大并发数,没有的话继续出队执行函数
    if (this.activeCount < this.count && this.queue.length > 0) {
      const func = this.queue.shift();
      func && func();
    }
  }
}
function asyncFun(value, delay) {
  return new Promise((resolve) => {
    console.log("start " + value);
    setTimeout(() => resolve(value), delay);
  });
}
const plimit = new PLimit(2);
const res = async () => {
  console.log("====================================");
  console.log(
    await plimit.promiseAll([
      () => asyncFun("aaa", 2000),
      () => asyncFun("bb", 3000),
      () => asyncFun("ccc", 1000),
      () => asyncFun("ccc", 1000),
      () => asyncFun("ccc", 1000),
    ])
  );
  console.log("====================================");
};
res();

还有很多细节问题我可能没有注意到,欢迎在评论区留言指出~

参考:

zhuanlan.zhihu.com/p/604178057…