前言
在写这篇文章的时候我有点犹豫,因为先前写过一篇类似的,一道关于并发控制的面试题,只不过那篇文章只给出了一种解决方案,后来在网上又陆续找到两种解决方案,说来惭愧,研究问题总是浅尝辄止,所以今天便放在一起,借着这道面试题再重新梳理一下。
题目是这样的:
有 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的,关键在于灵活运用,通过这次梳理,你有哪些思考呢
总结
到此这篇关于Promise面试题详解之控制并发的文章就介绍到这了,更多相关Promise控制并发内容请搜索自由互联以前的文章或继续浏览下面的相关文章希望大家以后多多支持自由互联!