前言

众所周知,Promise处理异步任务能避免他们阻塞程序执行。当一次并发大量异步任务会导致内存消耗过大、程序阻塞等问题。本文带大家实现异步任务控制器,限制并发异步任务数量,来解决高并发问题。
假设一个场景:有20个异步任务,每次只能处理三个异步任务,要求尽可能快速的拿到处理结果。
下面分为Promise.all异步任务控制器两种实现方案。

Promise.all

暴力Promise.all

最简单的方式就是Promise.all,一次并发20个任务,没有使用异步任务控制,简单、粗暴。

// 模拟请求 随机产生100-500ms延时
function randomRequest(url){
return new Promise((resolve)=>{
let delay = Math.floor(Math.random()*400+100)
setTimeout(()=>{
resolve({state:'success',data:{url}})
},delay)
})
}
async function main(){
const queue = [];
for (let i = 1; i <= 20; i++) {
queue.push(randomRequest(`https://xxx.xx/api/${i}`));
}
let a = await Promise.all(queue)
console.log(a);
}
mian()

分段Promise.all

这种分段的方式,有两个显著的缺陷:

  1. 阻塞问题:因为程序每次都要等异步任务全执行完,才进行下个异步任务,其中一个异步任务发生阻塞则会导致整体阻塞。
  2. 无法处理reject: 一旦有一个 Promise 被拒绝就立即返回拒绝的 Promise,并不会等待其他 Promise 的解析结果
// 模拟请求 随机产生100-500ms延时
function randomRequest(url){
return new Promise((resolve)=>{
let delay = Math.floor(Math.random()*400+100)
setTimeout(()=>{
resolve({state:'success',data:{url}})
},delay)
})
}
async function main(maxNum){
const queue = [];
for (let i = 1; i <= 20; i++) {
queue.push(randomRequest(`https://xxx.xx/api/${i}`));
}
for(let i=0;i<Math.ceil(queue.length/maxNum);i++){
let a = await Promise.all(queue.slice(i*maxNum,i*maxNum+maxNum))
console.log(a);
}
}
main(3)

异步任务控制器

开始就并发3个数量的一次任务,当一个异步任务处理完成,接龙下个异步任务,就像3条流水线并行。解决了Promise.all带来的阻塞问题和无法处理reject问题。
实现需要注意:

  • urls的长度为0时,results就没有值,此时应该返回空数组
  • maxNum大于urls的长度时,应该取的是urls的长度,否则则是取maxNum
  • 需要定义一个count计数器来判断是否已全部请求完成
  • 因为没有考虑请求是否请求成功,所以请求成功或报错都应把结果保存在results集合中
  • results中的顺序需和urls中的保持一致
// 模拟请求 0.5概率成功,随机产生100-500ms延时
function randomRequest(url){
return new Promise((resolve,reject)=>{
let delay = Math.floor(Math.random()*400+100)
setTimeout(()=>{
let rand = Math.random()
if(rand>0.5) resolve({state:'success',data:{url}})
else reject({state:'error'})
},delay)
})
}

// 并发控制函数
const controlAsync = (urls, maxNum) => {
return new Promise((resolve) => {
if (urls.length === 0) {
resolve([]);
return;
}
const results = [];
let index = 0; // 下一个请求的下标
let count = 0; // 当前请求完成的数量

// 发送请求
async function request() {
if (index === urls.length) return;
const i = index; // 保存序号,使result和urls相对应
const url = urls[index];
index++;
console.log(url);
try {
const resp = await randomRequest(url);
// resp 加入到results
results[i] = resp;
} catch (err) {
// err 加入到results
results[i] = err;
} finally {
count++;
// 判断是否所有的请求都已完成
if (count === urls.length) {
console.log('完成了');
resolve(results);
}
request();
}
}
// maxNum和urls.length取最小进行调用
const times = Math.min(maxNum, urls.length);
for(let i = 0; i < times; i++) {
request();
}
})
}

const urls = [];
for (let i = 1; i <= 20; i++) {
urls.push(`https://xxx.xx/api/${i}`);
}
controlAsync(urls, 3).then(res => {
console.log(res);
})

总结

异步任务控制器Promise.all实现复杂一些,但能解决阻塞问题和reject问题;
在工作中可以将异步任务控制器封装成通用的工具函数,实现多种异步任务的并发控制。


感谢小伙伴们的耐心观看,本文为笔者个人学习记录,如有谬误,还请告知,万分感谢!如果本文对你有所帮助,还请点个关注点个赞~,您的支持是笔者不断更新的动力!