这是一道常见面试题,类似下图:
我面试被问到过不止一次,举个例子:有 3 个异步任务 a,b 和 c,a 花费 1.5s 完成,b 花费 0.5s 完成,c 花费 1s 完成,但最多只能 2 个任务同时执行,现在想让你尽快的执行完 3 个任务并且获取到结果。
如果没有「2 个任务同时执行」的限制,那么直接 3 个任务一起,调用 Promise.all 即可,但是现在不行,那这样,我先 Promise.all([a,b]),结束之后再Promise.all([c]),这不就保证了「2 个任务同时执行」,但还是不行,因为没有保证「尽快的执行完」,你这样执行需要花费 1.5+1=2.5 秒的时间,就是你把 b 和 c 换位置,那也需要 1.5+0.5=2 秒的时间,如果真的尽快的执行完,那么只需要 1.5 秒的时间。所以如何解决?
重点在于 2 个任务 a,b 在同时执行中,其中 a 先执行完了,那么就应该马上执行后面排队的任务 c,而不是要等 a 和 b 都执行完了,才去执行 c。大圣有对此问题的解法,我们来看看:
首先写个异步函数 sleep,他的功能是延迟一定时间之后 resolve,并输出开始和结束的时间
const now = Date.now()
async function sleep(n, name) {
return new Promise((resolve, reject) => {
console.log(n, name, 'start', Date.now() - now)
setTimeout(() => {
console.log(n, name, 'end', Date.now() - now)
resolve({ n, name })
}, n * 500);
})
}
再写个调用函数 start,功能是调用并发控制函数 asyncPool,并传入异步函数数组:
async function start() {
await asyncPool({
limit: 2,
items: [
() => sleep(3, '3'),
() => sleep(1, '1'),
() => sleep(2, '2'),
]
})
}
start()
最后看看asyncPool的实现:
async function asyncPool({ limit, items }) {
// 保存所有的异步函数
const promises = []
// 异步函数执行池,如limit为2时,这里只有2个同时执行的异步函数
// 使用Set的原因:可以自动去除重复的异步函数
const pool = new Set()
for (const item of items) {
// 生成promise函数,要使用async包装下的原因是:可能item不是异步函数
const fn = async item => await item()
// 调用异步函数item并拿到引用
const promise = fn(item)
// 放到异步函数列表中
promises.push(promise)
// 放到异步函数执行池中
// 问题:这里直接放入异步函数执行池中,是不是有可能超出limit限制个数?
// 答:不会,因为下面有 await Promise.race 这会阻止 pool.add 的连续执行
pool.add(promise)
// 定义执行池的清除函数
const clean = () => pool.delete(promise)
// 不管成功还是失败,都执行清除,给下一个异步函数让位置
promise.then(clean).catch(clean)
if (pool.size >= limit) {
// 当执行池的个数大于等于限制个数时,就要等待一下,即把执行池中的函数进行比赛
// 先执行完的就让出位置,给后面的异步函数
await Promise.race(pool)
}
}
// asyncPool要求返回值是promise
return Promise.all(promises)
}
我认为这里巧妙的有 2 点
- 使用 for 和 await 结合,等待执行
- 对已经结束或执行中的异步函数使用 Promise.race 和 Promise.all 获取状态和结果
第二点你可能有疑问,我举个例子:执行到 return Promise.all(promises) 这一行时,promises 中的大部分异步函数都已经执行完了,但他还是能拿到执行完后的结果,看个示例:
(async function main() {
const s1 = sleep(1, 'ha')
const s2 = sleep(2, 'xi')
let ans = await Promise.all([
s1,
s2
])
console.log('ans', ans)
ans = await Promise.all([
s1,
s2
])
console.log('ans', ans)
})()
输出
1 'ha' 'start' 1
2 'xi' 'start' 3
1 'ha' 'end' 505
2 'xi' 'end' 1006
ans [ { n: 1, name: 'ha' }, { n: 2, name: 'xi' } ]
ans [ { n: 1, name: 'ha' }, { n: 2, name: 'xi' } ]
对于第一个 Promise.all 他会等待 s1 和 s2 执行完成后得到结果,但是对于第二个 Promise.all 他就不会执行了,因为 Promise 已经从 pending 到 fufilled 了,所以就直接返回答案。Promise.race 也是一样,你可能在这些异步任务完成期间多次调用了 Promise.race,但他们的结果都是一样的,并且也不会多次执行。
好啦,这个题目算是完成了,但还可以扩展下:有个库可以直接解决此类,即 p-limit 他的源码很少,几十行,中心思想和我们类似,都是在异步任务执行池中有一个结束时,就开始执行异步任务队列的其他问题。