在业务开发过程中,我们经常会遇到多个异步任务并发执行的情况,待所有异步任务结束之后再执行我们的业务逻辑。
通常情况下,我们会采用 ES6 标准下的Promise.all([promise1, promise2, promise3,...]).then( )
方法来应对这样的场景需求,
Promise.all
可以保证,promises 数组中所有 promise 对象都达到 resolve 状态,才执行 then 回调。
这样的 promise 对象可能是你发出的 http 请求,亦或是普通的库表查询操作,看起来平平常常没什么了不起,但是如果这样的 promise 不是一次来个三五个,而是成百上千个地同时出现,不好意思,小船说翻就翻。为什么?因为你在瞬间发出了大量的 http 请求(tcp 连接数不足可能造成等待),或者堆积了无数调用栈导致内存溢出。
这个时候,我们就需要对 Promise.all
做出限制了,我们要限制单次并发量,但最终的执行结果还是和常规的 Promise.all
保持一致。
从工具开发者的角度来思考,我们不能限制用户想要执行的异步任务数量,但是我们可以规定单次并发执行的 promise 数量,更进一步讲就是控制 promise 的实例化数量,以规避高并发带来的种种问题。当本次 promise 全部 resolve 或者有单个 promise 最先达到 resolve 状态,再将余下的 promise 依次放入队列。
GitHub 上有不少针对该功能实现的开源项目,并已经发布到 npm 上,比如,async-pool、p-limit 以及功能比较丰富的 es6-promise-pool,在这里我们选取前 2 个项目来进行简要分析。
asyncPool 提供了两种实现,一种是基于 ES6 标准的 Promise,另外一种是利用 ES7 的 async 函数来实现。
/**
*
* @param { 并发限制 } poolLimit
* @param { promise 数组 } array
* @param { callback } iteratorFn
*/
function asyncPool(poolLimit, array, iteratorFn) {
let i = 0
const ret = []
const executing = []
const enqueue = function () {
// ① 边界条件,array 为空或者 promise 都已达到 resolve 状态
if (i === array.length) {
return Promise.resolve()
}
const item = array[i++]
// ② 生成一个 promise 实例,并在 then 方法中的 onFullfilled 函数里返回实际要执行的 promise,
const p = Promise.resolve().then(() => iteratorFn(item, array))
ret.push(p)
// ④ 将执行完毕的 promise 移除
const e = p.then(() => executing.splice(executing.indexOf(e), 1))
// ③ 将正在执行的 promise 插入 executing 数组
executing.push(e)
let r = Promise.resolve()
// ⑥ 如果正在执行的 promise 数量达到了并发限制,则通过 Promise.race 触发新的 promise 执行
if (executing.length >= poolLimit) {
r = Promise.race(executing)
}
// ⑤ 递归执行 enqueue,直到满足 ①
return r.then(() => enqueue())
}
return enqueue().then(() => Promise.all(ret))
}
以上代码大致按执行顺序做了注释,总结起来有以下 4 点:
demo
import asyncPool from "tiny-async-pool";
const timeout = i => new Promise(resolve => setTimeout(() => resolve(i), i));
return asyncPool(2, [1000, 5000, 3000, 2000], timeout).then(results => {
...
});
直接上代码:
async function asyncPool(poolLimit, array, iteratorFn) {
const ret = []
const executing = []
for (const item of array) {
const p = Promise.resolve().then(() => iteratorFn(item, array))
ret.push(p)
const e = p.then(() => executing.splice(executing.indexOf(e), 1))
executing.push(e)
if (executing.length >= poolLimit) {
await Promise.race(executing)
}
}
return Promise.all(ret)
}
实现方案和 es6 没有任何区别,但是得益于 async 函数简洁的语法,代码行数和逻辑清晰度上了不止一个台阶。
demo
import asyncPool from 'tiny-async-pool'
const timeout = (i) => new Promise((resolve) => setTimeout(() => resolve(i), i))
const results = await asyncPool(2, [1000, 5000, 3000, 2000], timeout)
p-limit 包的实现思路可以说和 async-pool 有异曲同工之妙,本质上都是控制 promise 实例化的数量,只是 p-limit 并没有将Promise.all
封装进去,也没有使用Promise.race
,而是利用 then 方法获得当前 promise 的执行情况,来决定是否将下一个 promise 放入执行队列。
const pLimit = (concurrency) => {
if (!((Number.isInteger(concurrency) || concurrency === Infinity) && concurrency > 0)) {
return Promise.reject(new TypeError('Expected `concurrency` to be a number from 1 and up'))
}
const queue = []
let activeCount = 0
const next = () => {
activeCount--
if (queue.length > 0) {
queue.shift()()
}
}
const run = (fn, resolve, ...args) => {
activeCount++
const result = pTry(fn, ...args)
resolve(result)
result.then(next, next)
}
const enqueue = (fn, resolve, ...args) => {
if (activeCount < concurrency) {
run(fn, resolve, ...args)
} else {
queue.push(run.bind(null, fn, resolve, ...args))
}
}
const generator = (fn, ...args) => new Promise((resolve) => enqueue(fn, resolve, ...args))
return generator
}
这里 p-try 是作者的另外一个 npm 包,主要用来实例化 promise 并 resolve 传入函数的执行结果,源码也很简单:
const pTry = (fn, ...arguments_) =>
new Promise((resolve) => {
resolve(fn(...arguments_))
})
p-limit 的使用方式:
const pLimit = require('p-limit')
const limit = pLimit(1)
const input = [limit(() => fetchSomething('foo')), limit(() => fetchSomething('bar')), limit(() => doSomething())]
;(async () => {
// Only one promise is run at once
const result = await Promise.all(input)
console.log(result)
})()
针对 p-limit 的执行过程做一下总结:
Promise.all
返回所谓 promise 并发限制,其实根源上就是控制 promise 的实例化。如果是通过第三方函数,那么就把创建 promise 的控制权交给第三方即可。
或者,另外一种更简单的思路是,将大量的异步任务分而治之,即每次 Promise.all
一部分任务,待所有异步任务都处理完毕,再将所有执行结果 return 出去。