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!

推薦閱讀: