使用Promise完成数据的限制并发请求

本文介绍了如何使用Promise和Promise.all来控制并发请求的数量,以避免瞬间大量请求导致的网络问题和内存溢出。通过创建一个PromisePool,将任务按限制的并发数分批执行,确保在所有任务完成后才执行后续操作。还提供了一个名为Parallel的函数实现,用于控制并发任务,当一个任务完成时,自动发起新的任务,直到所有请求完成。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

需求:需要保证代码在多个异步处理之后执行,我们通常会使用

Promise.all(promises: []).then(fun: function);

Promise.all可以保证,promises数组中所有promise对象都达到resolve状态,才执行then回调

那么会出现的情况是,你在瞬间发出几十万http请求(tcp连接数不足可能造成等待),或者堆积了无数调用栈导致内存溢出.

这个时候需要我们对HTTP的连接数做限制。

实现

//promise并发限制
class PromisePool {
    constructor(max, fn) {
        this.max = max; //最大并发量
        this.fn = fn; //自定义的请求函数
        this.pool = []; //并发池
        this.urls = []; //剩余的请求地址
    }
    start(urls) {
        this.urls = urls; //先循环把并发池塞满
        while (this.pool.length < this.max) {
            let url = this.urls.shift();
            this.setTask(url);
        }
        //利用Promise.race方法来获得并发池中某任务完成的信号
        let race = Promise.race(this.pool);
        return this.run(race);
    }
    run(race) {
        race
            .then(res => {
                //每当并发池跑完一个任务,就再塞入一个任务
                let url = this.urls.shift();
                this.setTask(url);
                return this.run(Promise.race(this.pool));
            })
    }
    setTask(url) {
        if (!url) return
        let task = this.fn(url);
        this.pool.push(task); //将该任务推入pool并发池中
        console.log(`\x1B[43m ${url} 开始,当前并发数:${this.pool.length}`)
        task.then(res => {
            //请求结束后将该Promise任务从并发池中移除
            this.pool.splice(this.pool.indexOf(task), 1);
            console.log(`\x1B[43m ${url} 结束,当前并发数:${this.pool.length}`);
        })
    }
}
//test
const URLS = [
    'bytedance.com',
    'tencent.com',
    'alibaba.com',
    'microsoft.com',
    'apple.com',
    'hulu.com',
    'amazon.com'
]
//自定义请求函数
var requestFn = url => {
    return new Promise(resolve => {
        setTimeout(() => {
            resolve(`任务${url}完成`)
        }, 1000)
    }).then(res => {
        console.log('外部逻辑', res);
    })
}
const pool = new PromisePool(5, requestFn); //并发数为5
pool.start(URLS)

可以看出,思路如下:

定义一个 PromisePool 对象,初始化一个 pool 作为并发池,然后先循环把并发池塞满,不断地调用 setTask
然后通过自己自定义的任务函数(任务函数可以是网络请求封装的 promise
对象,或者是其他的),而且每个任务是一个Promise对象包装的,执行完就 pop 出连接池, 任务push 进并发池 pool 中。

//利用Promise.race方法来获得并发池中某任务完成的信号
let race = Promise.race(this.pool);
return this.run(race);
run(race) {
    race
        .then(res => {
            //每当并发池跑完一个任务,就再塞入一个任务
            let url = this.urls.shift();
            this.setTask(url);
            return this.run(Promise.race(this.pool));
        })
}

这个地方就是不断通过递归的方式,每当并发池跑完一个任务,就再塞入一个任务

实现一个并发函数

要求:实现一个 Parallel 函数,可以控制并发任务的数量,实现效果如下

function task (ms, name) {
  return new Promise((resolve, reject) => {
    setTimeout(_ => {
      console.log(new Date(), name);
      resolve();
    }, ms)
  })
}
console.log('task start-----------', new Date());
const para = new Parallel(3);
// 载入任务
para.addTask(task, 1000, 'a');
para.addTask(task, 2000, 'b');
para.addTask(task, 3000, 'c');
para.addTask(task, 4000, 'd');
// 开始执行
para.start();
// 并发数量为 3
// 分别于 1 2 3 5 秒后执行上述任务
// 并发数量为 2
// 则分别于 1 2 4 6 秒后执行上述任务

解答:

function task (ms, name) {
  return new Promise((resolve, reject) => {
    setTimeout(_ => {
      console.log(new Date(), name);
      resolve();
    }, ms)
  })
}

function Parallel (number) {
  this.number = number;
  this.currentNumber = 0;
  this.taskList = [];
  this.carryTaskList = [];
}
Parallel.prototype.addTask = function (task, ms, name) {
  this.taskList.push({
    fn: task,
    ms: ms,
    name: name
  })
}
Parallel.prototype.start = function () {
  if (this.taskList.length <= 0) {
    return;
  }
  while (this.currentNumber < this.number) {
    const newTask = this.taskList.shift();
    this.carryTaskList.push(newTask);
    this.currentNumber++;
  }
  this.carry();
}
Parallel.prototype.carry = function () {
  while (this.carryTaskList.length > 0) {
    const newTask = this.carryTaskList.shift();
    newTask.fn(newTask.ms, newTask.name).then(_ => {
      this.currentNumber--;
      this.start();
    })
  }
}

console.log('task start-----------', new Date());
const para = new Parallel(3);
para.addTask(task, 1000, 'a');
para.addTask(task, 2000, 'b');
para.addTask(task, 3000, 'c');
para.addTask(task, 4000, 'd');
para.start();

并发请求Promise.all实现

Promise.all 的简单解释:

// 当以下数组中promise1, promise2, promise3都resolve之后,触发promise.all的then函数。
Promise.all([promise1, promise2, promise3]).then((values) => {
  console.log(values);
});

需求解释

所谓并发请求,即有待请求接口100个,限制每次只能发出10个。即同一时刻最多有10个正在发送的请求。

每当10个之中有一个请求完成,则从待请求的接口中再取出一个发出。保证当前并发度仍旧为10。

直至最终请求完成

简单思路如下:(假设并发请求函数名字为limitedRequest)

  • 设定一个数组(命名为:pool),用于后续Promise.all的使用
    当limitedRequest被调用的时候,首先一次性发出10个请求,并放入到pool中
  • 每一个请求完成后的回调函数中继续触发下一个请求,而下一个请求返回Promise,他的回调函数继续绑定同样的回调函数,即循环调用。(看不懂就直接看代码更易懂)
  • 直到全部请求完成,停止。
// 模仿一个fetch的异步函数,返回promise
function mockFetch(param) {
  return new Promise((resovle) => {
    setTimeout(() => {
      resovle(param);
    }, 2000);
  });
}

function limitedRequest(urls, maxNum) {
  const pool = [];
  // 处理maxNum比urls.length 还要大的情况。
  const initSize = Math.min(urls.length, maxNum);
  for (let i = 0; i < initSize; i++) {
  	// 一次性放入初始的个数
    pool.push(run(urls.splice(0, 1)));
  }
  // r 代表promise完成的resolve回调函数
  // r 函数无论什么样的结果都返回promise,来确保最终promise.all可以正确执行
  function r() {
    console.log('当前并发度:', pool.length);
    if (urls.length === 0) {
      console.log('并发请求已经全部发起');
      return Promise.resolve();
    }
    return run(urls.splice(0, 1));
  }
  // 调用一次请求
  function run(url) {
    return mockFetch(url).then(r);
  }
  // 全部请求完成的回调
  Promise.all(pool).then(() => {
    console.log('请求已经全部结束');
  });
}
// 函数调用
limitedRequest([1, 2, 3, 4, 5, 6, 7, 8], 3);
# 最终返回结果
$ node .\src\views\doc\detail\index.js
当前并发度: 3
当前并发度: 3
当前并发度: 3
当前并发度: 3
当前并发度: 3
当前并发度: 3
并发请求已经全部发起
当前并发度: 3
并发请求已经全部发起
当前并发度: 3
并发请求已经全部发起
请求已经全部结束
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值