Promise并发控制

Wanjiongming 于 2021-12-28 发布

JavaScript 虽说是单线程的,但是由于存在异步任务任务队列,所以还是会遇到和后端语言(比如 Java)一样的并发控制问题

在平时开发中,实现并发最常用的就是使用Promise.all(),但是Promise.all()有一个很大的缺点,就是所有任务都是同时进行无法对任务进行并发控制

一个最常见的需求如批量上传图片,在选中多张图片后期望并发上传,同时考虑到客户端网络和服务端负载情况(假设选了 100 张图片,同时上传肯定会有超时错误产生),期望有同时运行的上传任务不超过 2

基于 Promise 可以写出如下的任务队列

class TaskQueue {
  constructor(maxNum) {
    this.list = []
    this.maxNum = maxNum
    this.workingNum = 0
  }

  add(asyncFunction) {
    this.list.push(asyncFunction)
    this.run()
  }

  run() {
    for (let i = 0; i < this.maxNum; i++) {
      this.doNext()
    }
  }

  doNext() {
    if (this.list.length == 0 && this.workingNum == 0) {
      console.timeEnd('task')
    }
    if (this.list.length && this.workingNum < this.maxNum) {
      this.workingNum++
      const asyncFunction = this.list.shift()
      asyncFunction().finally(() => {
        this.workingNum--
        this.doNext()
      })
    }
  }
}

测试代码

function createTask(name, delay) {
  return () => {
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        console.log(name, 'done')
        resolve()
      }, delay)
    })
  }
}

const queue = new TaskQueue(2)

console.time('task')
queue.add(createTask('task1', 1000))
queue.add(createTask('task2', 1000))
queue.add(createTask('task3', 1000))
queue.add(createTask('task4', 1000))

拓展

在百度的过程中发现这个和字节的一个面试题很像 某条高频面试原题:实现有并行限制的 Promise 调度器

另 Github 上也有现成的方案 sindresorhus/p-queue 可以实现更加强大的功能。发现作者基于 promise 封装了好多有意思的工具库 sindresorhus/promise-fun,有兴趣的可以学习下

上面的问题使用p-queue可以更加优雅地完成

const queue = new PQueue({ concurrency: 2, autoStart: true })

console.time('task')
queue.add(createTask('task1', 1000))
queue.add(createTask('task2', 1000))
queue.add(createTask('task3', 1000))
queue.add(createTask('task4', 1000))

await queue.onIdle()
console.timeEnd('task')