标准答案

  1. 并发控制不是 Promise.all,Promise.all 会一次性启动所有任务。
  2. 需要维护待执行队列、当前运行数量和最大并发数。
  3. 每个任务完成或失败后都要减少运行数量,并继续调度队列中的下一个任务。
  4. 结果通常要按输入顺序保存,而不是按完成顺序保存。
  5. 实际请求场景还要考虑取消、重试、超时、失败策略和页面卸载。

题目解析

并发控制常见于批量上传、批量下载、批量接口补数据。限制并发不是为了让代码更复杂,而是为了避免瞬间打爆浏览器连接数、后端限流或用户网络。

实现时要把“启动任务”和“任务完成后的继续调度”拆开。每完成一个任务,就空出一个并发名额,这时才应该拉起下一个任务。

错误策略要提前想清楚。某个任务失败后,是整体中断、继续执行剩余任务,还是记录失败结果后统一返回,这取决于业务场景。

代码示例

下面的实现会限制最多同时运行 limit 个任务,并按输入顺序返回结果。

JavaScript
function runWithLimit(tasks, limit) {
  return new Promise((resolve, reject) => {
    const results = []
    let nextIndex = 0
    let running = 0
    let finished = 0

    if (limit < 1) {
      reject(new RangeError('limit must be greater than 0'))
      return
    }

    function schedule() {
      if (finished === tasks.length) {
        resolve(results)
        return
      }

      while (running < limit && nextIndex < tasks.length) {
        const currentIndex = nextIndex++
        running += 1

        Promise.resolve()
          .then(() => tasks[currentIndex]())
          .then(value => {
            results[currentIndex] = value
            finished += 1
            running -= 1
            schedule()
          }, reject)
      }
    }

    schedule()
  })
}

常见误区

  • 先把所有请求 Promise 创建出来,再做并发控制。这样请求可能已经全部开始了。
  • 任务失败后没有释放运行名额,队列卡住。
  • 结果按完成顺序 push,调用方无法对应原始任务。

作者信息