Promise面試題詳解之控制並發
前言
在寫這篇文章的時候我有點猶豫,因為先前寫過一篇類似的,一道關於並發控制的面試題,隻不過那篇文章隻給出瞭一種解決方案,後來在網上又陸續找到兩種解決方案,說來慚愧,研究問題總是淺嘗輒止,所以今天便放在一起,借著這道面試題再重新梳理一下。
題目是這樣的:
有 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控制並發內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!
推薦閱讀:
- 字節飛書面試promise.all實現示例
- 徹底搞懂 javascript的Promise
- Promise靜態四兄弟實現示例詳解
- 前端JavaScript之Promise
- vue中Promise的使用方法詳情