Promise面試題詳解之控制併發
前言
在寫這篇文章的時候我有點猶豫,因為先前寫過一篇類似的,一道關於併發控制的面試題,只不過那篇文章只給出了一種解決方案,後來在網上www.cppcns.com又陸續找到兩種解決方案,說來慚愧,研究問題總是淺嘗輒止,所以今天便放在一起,藉著這道面試題再重新梳理一下。
題目是這樣的:www.cppcns.com
有 8 個圖片資源的 url,已經儲存在陣列 urls 中(即urls = [‘http://example.com/1.jpg',….,‘http://example.com/8.jpg']),而且已經有一個函式 function loadImg,輸入一個 url 連結,返回一個 Promise,該 Promise 在圖片下載完成的時候 resolve,下載失敗則 reject。
但是我們要求,任意時刻,同時下載的連結數量不可以超過 3 個。
請PpCFCWkXV寫一段程式碼實現這個需求,要求儘可能快速地將所有圖片下載完成。
已有程式碼如下:
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) }) } })() } http://www.cppcns.comlimitLoad(urls,loadImg,3)
第三種方案的巧妙之處,在於使用了Promise.race。並且在迴圈時用then鏈串起了執行順序。
以上便是關於併發控制的一點點思考,有使用promise的,有不使用promise的,關鍵在於靈活運用,通過這次梳理,你有哪些思考呢
總結
到此這篇關於Promise面試題詳解之控制併發的文章就介紹到這了,更多相關Promise控制併發內容請搜尋我們以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援我們!