Promise控制并发 Promise面试题详解之控制并发
童红雷 人气:0前言
在写这篇文章的时候我有点犹豫,因为先前写过一篇类似的,一道关于并发控制的面试题,只不过那篇文章只给出了一种解决方案,后来在网上又陆续找到两种解决方案,说来惭愧,研究问题总是浅尝辄止,所以今天便放在一起,借着这道面试题再重新梳理一下。
题目是这样的:
有 8 个图片资源的 url,已经存储在数组 urls 中(即urls = [‘http://example.com/1.jpg', …., ‘http://example.com/8.jpg']),而且已经有一个函数 function loadImg,输入一个 url 链接,返回一个 Promise,该 Promise 在图片下载完成的时候 resolve,下载失败则 reject。
但是我们要求,任意时刻,同时下载的链接数量不可以超过 3 个。
请写一段代码实现这个需求,要求尽可能快速地将所有图片下载完成。
已有代码如下:
var urls = [ 'https://www.kkkk1000.com/images/getImgData/getImgDatadata.jpg', 'https://www.kkkk1000.com/images/getImgData/gray.gif', 'https://www.kkkk1000.com/images/getImgData/Particle.gif', 'https://www.kkkk1000.com/images/getImgData/arithmetic.png', 'https://www.kkkk1000.com/images/getImgData/arithmetic2.gif', 'https://www.kkkk1000.com/images/getImgData/getImgDataError.jpg', 'https://www.kkkk1000.com/images/getImgData/arithmetic.gif', 'https://www.kkkk1000.com/images/wxQrCode2.png' ]; function loadImg(url) { return new Promise((resolve, reject) => { const img = new Image() img.onload = function () { console.log('一张图片加载完成'); resolve(); } img.onerror = reject img.src = url }) };
看到这个题目的时候,脑袋里瞬间想到了高效率排队买地铁票的情景,那个情景类似下图:
上图这样的排队和并发请求的场景基本类似,窗口只有三个,人超过三个之后,后面的人只能排队了。
首先想到的便是利用递归来做,就如这篇文章采取的措施一样,代码如下:
//省略代码 var count = 0; //对加载图片的函数做处理,计数器叠加计数 function bao(){ count++; console.log("并发数:",count) //条件判断,urls长度大于0继续,小于等于零说明图片加载完成 if(urls.length>0&&count<=3){ //shift从数组中取出连接 loadImg(urls.shift()).then(()=>{ //计数器递减 count-- //递归调用 }).then(bao) } } function async1(){ //循环开启三次 for(var i=0;i<3;i++){ bao(); } } async1()
以上是最常规的思路,我将加载图片的函数loadImg封装在bao函数内,根据条件判断,是否发送请求,请求完成后继续递归调用。
以上代码所有逻辑都写在了同一个函数中然后递归调用,可以优化一下,代码如下:
var count = 0; // 封装请求的异步函数,增加计数器功能 function request(){ count++; loadImg(urls.shift()).then(()=>{ count-- }).then(diaodu) } // 负责调度的函数 function diaodu(){ if(urls.length>0&&count<=3){ request(); } } function async1(){ for(var i=0;i<3;i++){ request(); } } async1()
上面代码将一个递归函数拆分成两个,一个函数只负责计数和发送请求,另外一个负责调度。
这里的请求既然已经被封装成了Promise,那么我们用Promise和saync、await来完成一下,代码如下:
//省略代码 // 计数器 var count = 0; // 全局锁 var lock = []; var l = urls.length; async function bao(){ if(count>=3){ //超过限制利用await和promise进行阻塞; let _resolve; await new Promise((resolve,reject)=>{ _resolve=resolve; // resolve不执行,将其推入lock数组; lock.push(_resolve); }); } if(urls.length>0){ console.log(count); count++ await loadImg(urls.shift()); count--; lock.length&&lock.shift()() } } for (let i = 0; i < l; i++) { bao(); }
大致思路是,遍历执行urls.length长度的请求,但是当请求并发数大于限制时,超过的请求用await结合promise将其阻塞,并且将resolve填充到lock数组中,继续执行,并发过程中有图片加载完成后,从lock中推出一项resolve执行,lock相当于一个叫号机;
以上代码可以优化为:
// 计数器 var count = 0; // 全局锁 var lock = []; var l = urls.length; // 阻塞函数 function block(){ let _resolve; return new Promise((resolve,reject)=>{ _resolve=resolve; // resolve不执行,将其推入lock数组; lock.push(_resolve); }); } // 叫号机 function next(){ lock.length&&lock.shift()() } async function bao(){ if(count>=3){ //超过限制利用await和promise进行阻塞; await block(); } if(urls.length>0){ console.log(count); count++ await loadImg(urls.shift()); count--; next() } } for (let i = 0; i < l; i++) { bao(); }
最后一种方案,也是我十分喜欢的,思考好久才明白,大概思路如下:
用 Promise.race来实现,先并发请求3个图片资源,这样可以得到 3 个 Promise实例,组成一个数组promises ,然后不断的调用 Promise.race 来返回最快改变状态的 Promise,然后从数组(promises )中删掉这个 Promise 对象实例,再加入一个新的 Promise实例,直到全部的 url 被取完。
代码如下:
//省略代码 function limitLoad(urls, handler, limit) { // 对数组做一个拷贝 const sequence = [].concat(urls) let promises = []; //并发请求到最大数 promises = sequence.splice(0, limit).map((url, index) => { // 这里返回的 index 是任务在 promises 的脚标, //用于在 Promise.race 之后找到完成的任务脚标 return handler(url).then(() => { return index }); }); (async function loop() { let p = Promise.race(promises); for (let i = 0; i < sequence.length; i++) { p = p.then((res) => { promises[res] = handler(sequence[i]).then(() => { return res }); return Promise.race(promises) }) } })() } limitLoad(urls, loadImg, 3)
第三种方案的巧妙之处,在于使用了Promise.race。并且在循环时用then链串起了执行顺序。
以上便是关于并发控制的一点点思考,有使用promise的,有不使用promise的,关键在于灵活运用,通过这次梳理,你有哪些思考呢
总结
加载全部内容