标准答案
- 并发控制不是 Promise.all,Promise.all 会一次性启动所有任务。
- 需要维护待执行队列、当前运行数量和最大并发数。
- 每个任务完成或失败后都要减少运行数量,并继续调度队列中的下一个任务。
- 结果通常要按输入顺序保存,而不是按完成顺序保存。
- 实际请求场景还要考虑取消、重试、超时、失败策略和页面卸载。
题目解析
并发控制常见于批量上传、批量下载、批量接口补数据。限制并发不是为了让代码更复杂,而是为了避免瞬间打爆浏览器连接数、后端限流或用户网络。
实现时要把“启动任务”和“任务完成后的继续调度”拆开。每完成一个任务,就空出一个并发名额,这时才应该拉起下一个任务。
错误策略要提前想清楚。某个任务失败后,是整体中断、继续执行剩余任务,还是记录失败结果后统一返回,这取决于业务场景。
代码示例
下面的实现会限制最多同时运行 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,调用方无法对应原始任务。