浅析如何实现一个并发请求控制函数并限制并发数

  面试题:

1、批量请求:要实现批量请求,而且并不需要按顺序发起请求(如果需要按顺序可以存入队列中,按优先级则可以存入优先队列中),所以这里我们存入数组中即可,然后进行遍历,取出数字中的每一项丢去fetch中进行调用。

2、可控制并发度:控制并发数,一个简单的办法就是对数组进行切片,分成一段一段,完成一段后再调用另一段。这里我们可以使用递归或者循环来实现,我觉得递归比较直观,所以这里使用递归来实现。

本题的难点就在这里,之前做错了就是这一步。在控制并发数的同时,每结束一个请求并发起一个新的请求。依旧使用递归的方式,但这次添加一个请求队列,然后我们只要维护这个队列,每次发起一个请求就添加进去,结束一个就丢出来,继而实现了控制并发。

3、全部请求结束,执行 callback:因为是异步请求,我们无法寄希望于安装正常的顺序在函数调用后执行,但是每次fetch有返回结果会调用then或者catch,我们可以在这个时候判断请求数组是否为空就可以知道是否全部被调用完

  做过爬虫的都知道,要控制爬虫的请求并发量,其实也就是控制其爬取频率,以免被封IP,还有的就是以此来控制爬虫应用运行内存,否则一下子处理N个请求,内存分分钟会爆。而 python 爬虫一般用多线程来控制并发,

  然而如果是 node.js 爬虫,由于其 单线程无阻塞 性质以及事件循环机制,一般不用多线程来控制并发(当然node.js也可以实现多线程,此处非重点不再多讲),而是更加简便地直接在代码层级上实现并发。

  为图方便,开发者在开发node爬虫一般会找一个并发控制的npm包,然而第三方的模块有时候也并不能完全满足我们的特殊需求,这时候我们可能就需要一个自己定制版的并发控制函数。

  下面我们用15行代码实现一个并发控制的函数。

1、参数

  首先,一个基本的并发控制函数,基本要有以下3个参数:

  • list {Array} - 要迭代的数组
  • limit {number} - 控制的并发数量
  • asyncHandle {function} - 对list的每一个项的处理函数

2、设计

  以下以爬虫为实例进行讲解:设计思路其实很简单,假如并发量控制是 5

(1)首先,瞬发 5 个异步请求,我们就得到了并发的 5 个异步请求

// limit = 5
while(limit--) {
    handleFunction(list)
}

(2)然后,这 5 个异步请求中无论哪一个先执行完,都会继续执行下一个list

let recursion = (arr) => {
    return asyncHandle(arr.shift()).then(()=>{
            // 迭代数组长度不为0, 递归执行自身
            if (arr.length!==0) return recursion(arr) 
            // 迭代数组长度为0,结束 
            else return 'finish';
        })
}

(3)等list所有的项迭代完之后的回调

return Promise.all(allHandle)

3、具体代码:上述步骤组合起来,就是

/**
 * @params list {Array} - 要迭代的数组
 * @params limit {Number} - 并发数量控制数
 * @params asyncHandle {Function} - 对`list`的每一个项的处理函数,参数为当前处理项,必须 return 一个Promise来确定是否继续进行迭代
 * @return {Promise} - 返回一个 Promise 值来确认所有数据是否迭代完成
 */
let mapLimit = (list, limit, asyncHandle) => {
    let recursion = (arr) => {
        return asyncHandle(arr.shift()).then(()=>{
                if (arr.length!==0) return recursion(arr)   // 数组还未迭代完,递归继续进行迭代
                else return 'finish';
            })
    };
    
    let listCopy = [].concat(list);
    let asyncList = []; // 正在进行的所有并发异步操作
    while(limit--) {
        asyncList.push( recursion(listCopy) ); 
    }
    return Promise.all(asyncList);  // 所有并发异步操作都完成后,本次并发控制迭代完成
}

  这里解释一下代码:

(1)recursion函数用来处理传参的回调函数asyncHandle,并将要迭代的数组作为参数传入,方便asyncHandle函数回调继续处理。

(2)recursion函数将要迭代的数组的第一个元素作为参数传给asyncHandle进行业务处理,asyncHandle需返回promise,才能进入 then 回调判断要迭代的数组是否迭代完,未迭代完的话就继续调用自身。

(3)concat深拷贝数组避免数据污染

4、测试demo

  模拟一下异步的并发情况

var dataLists = [1,2,3,4,5,6,7,8,9,11,100,123];
var count = 0;
mapLimit(dataLists, 3, (curItem)=>{
    return new Promise(resolve => {
        count++
        setTimeout(()=>{
            console.log(curItem, '当前并发量:', count--)
            resolve();
        }, Math.random() * 5000)  
    });
}).then(response => {
    console.log('finish', response)
})

  手动抛出异常中断并发函数测试:

var dataLists = [1,2,3,4,5,6,7,8,9,11,100,123];
var count = 0;
mapLimit(dataLists, 3, (curItem)=>{
    return new Promise((resolve, reject) => {
        count++
        setTimeout(()=>{
            console.log(curItem, '当前并发量:', count--)
            if(curItem > 4) reject('error happen')
            resolve();
        }, Math.random() * 5000)  
    });
}).then(response => {
    console.log('finish', response)
})

  并发控制情况下,迭代到5,6,7时,判断6>4,手动抛出异常,停止了后续迭代。

原文链接:https://segmentfault.com/a/1190000013128649

posted @ 2021-04-01 18:37  古兰精  阅读(2734)  评论(0编辑  收藏  举报