JS 并发控制的几种姿势

641 阅读9分钟

1.概念

Promise对象的状态不受外界影响 (3种状态)

  • Pending状态(进行中)
  • Fulfilled状态(已成功)
  • Rejected状态(已失败)

Promise.race

  • 那个返回快就返回那个promise,无论返回结果是成功还是失败。 注意点:
  1. 虽然已经返回最快的,但是其他的promise依然会继续进行。
  2. 传入参数promiseList集合可以是之前已经发起的,即可以重复继续传入Promise.race调用。
    let promiseList = [pro1,pro2,pro3];
    Promise.race(promiseList).then((ret) => {
        //1.假设pro2先结束,这时候pro1和pro3依然正在进行中
        //2.依然可以继续使用Promise.race 处理剩下的[pro1,pro3]
        let promiseList2 = [pro1,pro3];
         Promise.race(promiseList2).then((ret) => {
              ...
         })
    })

Promise.all

  • 把所有pormise成功执行完才返回对应所有集合的状态。但是如果遇到一个reject错误,就直接返回reject一个错误,不等其他。

for of

  • 是一个用于循环迭代可迭代的对象或集合,循环的时候可以动态给数据组移除或添加,动态变化循环。

2.asyncPool 学习

es7版

逻辑思想:

  • 通过一个动态的executing 实时保存着固定长度的 promise队列,每次通过同步 Promise.race抢出一个promise后,再加入一个,再Promise.race抢出一个,一直循环下去。
  • 最后再Promise.all的时候只剩下 poolLimit以内状态还是Pending 的内容在一起promise.all一起执行一遍,并等待结果返回所有成功数组。
  1. executing存储的是实时运行的队列,一般只会小于等于poolLimit 限制的长度。
  2. ret 记录所有promise的队列,用于最后把所有结果返回给用户。

缺点:

  • 如果限制条数过大,则可能promise.all的数量会很多
  • 如果遇到reject时候,不能发起重连,只能在结束后,再补发。

//poolLimit 限制并发的长度
//array 实际需要运行的所有任务集合
//iteratorFn 每一个任务需要执行的异步方法,返回promise对象

 async function asyncPool(poolLimit, array, iteratorFn) {
            const ret = []; // 存储所有的异步任务
            const executing = []; // 存储正在执行的异步任务
            for (const item of array) {
                // 调用iteratorFn函数创建异步任务,
                //这里用于解决promise.all 遇到reject只返回一个错误并中断问题。每个请求都是 resolve,确保promise.all能够全部执行完毕。
                const p = Promise.resolve().then(() => iteratorFn(item, array));
                ret.push(p); // 保存新的异步任务,记录所有promise的信息

                // 当poolLimit值小于或等于总任务个数时,进行并发控制
                if (poolLimit <= array.length) {
                    // 当任务完成后,从正在执行的任务数组中移除已完成的任务
                    const e = p.then(() => {
                        executing.splice(executing.indexOf(e), 1)
                    });
                    executing.push(e); // 保存正在执行的异步任务
                    if (executing.length >= poolLimit) { //当实际运行的长度 满足 限制的长度开始竞赛请求,注意这里里面可能包含之前已经处理一段时间的promise
                        await Promise.race(executing); // 等待较快的任务执行完成,这里响应最快结束的一个promise,然后继续循环加入。
                    }
                }
            }
            return Promise.all(ret);//到这里其实大部分promise已经都是Fulfilled,所有会直接返回 promise.all 把剩下 还是Pending 再执行一下,并等待结果返回所有成功数组。
        }
        
 //测试代码
 const timeout = i => new Promise(resolve => setTimeout(() => { 
      console.log("resolve",i)
      resolve(i) 
}, i));

async function test() { 
    let a ;
    try {
     a = await asyncPool(2, [1000, 5000, 3000, 2000], timeout);
    } catch (error) {
        console.log("error",error)
    }
   console.log("ret",a)
}
test()

//打印结果
//resolve 1000
//resolve 3000
//resolve 5000
//resolve 2000       
//ret [ 1000, 5000, 3000, 2000 ]


执行时长逻辑

  1. 0秒时候,一次只有2个任务并行,把1000 和 3000 任务加入队列
  2. 到第1秒时,第一个1000 结束,打印并移出,第二个3000 继续运行,同时加入新任务5000
  3. 到第3秒时,任务3000 结束,打印并移出,同时加入新任务2000
  4. 到第6秒时,任务5000 结束,打印并移出。
  5. 到第7秒时,任务2000 结束,打印并移出。

代码执行流程:

![image.png](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/cac66eeccfcc4f2bbf30b18e7d2ddd12~tplv-k3u1fbpfcp-watermark.image?)

executing的promise数组的整个变化过程:

image.png

es6版

逻辑思想:

  • 通过封装enqueue(),从i==0开始调用,当executing长度满足poolLimit 条件时,执行Promise.race,在executing取出最先返回的promise,在promise.then之后再递归重复enqueue()方法。直到i变量等于实际总长度,跳出循环。(由于es6没有async await ,只能通过地狱回调来实现)
function asyncPool(poolLimit, array, iteratorFn) {
    let i = 0;
    const ret = []; // 存储所有的异步任务
    const executing = []; // 存储正在执行的异步任务
    const enqueue = function () {
      if (i === array.length) {
        return Promise.resolve();
      }
      const item = array[i++]; // 获取新的任务项
      const p = Promise.resolve().then(() => iteratorFn(item, array));
      ret.push(p);
  
      let r = Promise.resolve();
  
      // 当poolLimit值小于或等于总任务个数时,进行并发控制
      if (poolLimit <= array.length) {
        // 当任务完成后,从正在执行的任务数组中移除已完成的任务
        const e = p.then(() => executing.splice(executing.indexOf(e), 1));
        executing.push(e);
        if (executing.length >= poolLimit) {
          r = Promise.race(executing); 
        }
      }
      // 正在执行任务列表 中较快的任务执行完成之后,才会从array数组中获取新的待办任务
      return r.then(() => enqueue());
    };
    return enqueue().then(() => Promise.all(ret));
  }
  //测试代码
  const timeout = i => new Promise((resolve,reject) => setTimeout(() => { 
    console.log("resolve",i) 
        resolve(i)  
}, i));
asyncPool(2, [1000, 5000, 3000, 2000], timeout).then((ret) => {
  console.log("ret",ret)
})

//resolve 1000
//resolve 3000
//resolve 5000
//resolve 2000
//ret [ 1000, 5000, 3000, 2000 ]

3.递归实现并发控制 es7

3.1简单版

原理: 通过while创建多个任务,每个任务都执行start()方法,start方法会一直通过 shift()方法拿到array列表最前面一条数据执行。每执行完一个任务就把counter++,当counter等于array原来长度 则代表所有并发已经结束。


async function sendRequest(limit=4,array,uploadReqFn){
      return new Promise((resolve,reject)=>{
        const len = array.length
        let counter = 0  
        let ret = [] //记录所有promise情况
        const start = async ()=>{
          const task = array.shift()
          if(task){ 
            ret.push(task)
              await  Promise.resolve().then( (res) => {
                return uploadReqFn(task)      
            })
              if(counter==len-1){
                // 最后一个任务
                resolve(Promise.all(ret)) // 同时把所有promise的执行结果都返回
              }else{
                counter++
                // 启动下一个任务
                start()
              } 
          }
        }
        //通过while创建多个(默认4个)固定的递归循环,start方法会一直递归start方法不断请求。
        while(limit>0){  
            start() 
            limit-=1
        }
      })
    }

const timeout = i => new Promise((resolve,reject) => setTimeout(() => { 
    console.log("resolve",i)
    resolve(i) 
}, i));

async function test() {
    try { 
     let a = await sendRequest(2, [1000, 5000, 3000, 2000], timeout);
     console.log("ret",a)
    } catch (error) {
        console.log("error",error)
    }
}
test()

//输出
//resolve 1000
//resolve 3000
//resolve 5000
//resolve 2000
//ret [ 1000, 5000, 3000, 2000 ]

3.2兼容错误异常处理版

当遇到一个start任务失败时自动重新发起,利用unshift优先插入错误重连的任务。 当超过4次,则当前的所有start请求都终止执行。


     async function sendRequest(limit=4,array,uploadReqFn){
        return new Promise((resolve,reject)=>{
          const len = array.length
          let counter = 0 
          let isStop = false
          let ret = [] //记录所有promise情况 
          const start = async ()=>{
            if(isStop){
              return 
            }
            const task = array.shift()
            if(task){ 
              ret.push(task)
              try{ 
                await  Promise.resolve().then( (res) => {
                    return uploadReqFn(task.time)      
                }) 
                if(counter==len-1){
                  // 最后一个任务
                    resolve(Promise.all(ret)) // 同时把所有promise的执行结果都返回
                }else{
                  counter++
                  // 启动下一个任务
                  start()
                }
              }catch(e){
                if(task.error<3){
                  task.error++
                  array.unshift(task)
                  start()
                }else{
                  // 错误三次
                  isStop = true
                  reject(Promise.all(ret))
                }
              }
            }
          }  
         //通过while创建多个(默认4个)固定的递归循环,start方法会一直递归start方法不断请求。
          while(limit>0){
            start() 
            limit-=1
         }
        })
      }

const timeout = i => new Promise((resolve,reject) => setTimeout(() => {  
    if (Math.random()> 0.3) {
        console.log("resolve",i) 
        resolve(i)  
    } else {
        console.log("reject",i) 
        reject(i)  
    }
}, i));

async function test() {  
    try { 
     let taskList = [
        {error:0,time:1000},
        {error:0,time:5000},
        {error:0,time:3000},
        {error:0,time:2000}
     ] 
     let a = await sendRequest(2, taskList, timeout);
     console.log("ret",a)
    } catch (error) {
        console.log("error",error)
    }
}
test()

//输出 这里3000 发生了一次异常错误,然后重新发起了请求
//resolve 1000
//reject 3000
//resolve 5000
//resolve 3000
//resolve 2000
//ret [ { error: 0, time: 1000 },
//  { error: 0, time: 5000 },
//  { error: 1, time: 3000 },
//  { error: 1, time: 3000 },
//  { error: 0, time: 2000 } ]

4.递归实现并发控制 es6版

逻辑思想: 通过while创建多个任务,每个任务都执行start()方法,start方法会一直通过count++往下不断请求。当遇到错误,则传入对应的错误索引继续请求。如:start(3) 重新发起3对应的请求

  
     function sendRequest(limit=4,array,uploadReqFn){
        const len = array.length
        const result = new Array(len).fill(false)
        let count = 0
        // 设置 error,记录错误的次数
        return new Promise((resolve, reject) => {
            while(count < limit){
                start()
            }
            function start(current = ''){
                if(current === ''){
                    current = count;
                    count++;
                } 
                if (current >= len) {//当游标到达实际的 集合总长度
                    // 请求全部完成就将promise置为成功状态, 然后将result作为promise值返回
                    !result.includes(false) && resolve(result);
                    return;
                }
                array[current].error++ 
                uploadReqFn(array[current]).then((res) => {
                    result[current] = res;
                    // 请求没有全部完成, 就递归
                    if (current < len) {
                        start();
                    }
                }).catch((err) => {
                    // 报错时,请求次数小于3时将重新发起请求
                    if(array[current].error < 3){
                        start(current)
                    }else{
                        result[current] = err;
                        // 请求没有全部完成, 就递归
                        if (current < len) {
                            start();
                        }
                    }
                });
            }
        })
    } 



    const timeout = i => new Promise((resolve,reject) => setTimeout(() => {  
        if (Math.random()> 0.5) {
            console.log("resolve",i) 
            resolve(i)  
        } else {
            console.log("reject",i) 
            reject(i)  
        }
    }, i));
    
    async function test() { 
        let a ;
        try { 
         let taskList = [
            {error:0,time:1000},
            {error:0,time:5000},
            {error:0,time:3000},
            {error:0,time:2000}
         ]
     
         a = await sendRequest(2, taskList, timeout);
        } catch (error) {
            console.log("error",error)
        }
       console.log("ret",a)
    }
    test()
    
    
//输出 这里 2000 发生了错误,并发起重连
//resolve { error: 1, time: 1000 }
//resolve { error: 1, time: 5000 }
//resolve { error: 1, time: 3000 }
//reject { error: 1, time: 2000 }
//resolve { error: 2, time: 2000 }
//ret [ { error: 1, time: 1000 },
//  { error: 1, time: 5000 },
//  { error: 1, time: 3000 },
//  { error: 2, time: 2000 } ]

4.2.递归实现2 class


class SuperTask {
    constructor(paralleCnt = 2){
        this.tasks = []
        this.runningCnt = 0
        this.paralleCnt = paralleCnt
    }
     add(task){
        return new Promise((resolve,reject) => {
            this.tasks.push({task,resolve,reject})
            // console.log("this.tasks",this.tasks)
            this._run()
        })
    }
    _run(){  
        while(this.runningCnt <this.paralleCnt && this.tasks.length) { 
            // console.log("xxx")
            const {task,resolve,reject} = this.tasks.shift()
            // console.log(task,resolve,reject)
            this.runningCnt++;

            task().then(resolve,reject).finally(() => {
                this.runningCnt--;
                this._run()
            })
        }
    }
}


const superTask = new SuperTask(2)
function addTask(time,name){
    superTask.add(() =>  
        timeout(time) ).then(() => {
        console.log(`${name}任务被执行`)
    })
}
function timeout(time){
    return new Promise(resolve => {
        setTimeout(() => {
            resolve(time) 
        }, time);
    })
} 
addTask(1000,1)
addTask(5000,2)
addTask(2000,3)
addTask(3000,4)
addTask(1000,5)

5.Promise.all 处理reject问题

核心思想: 把集合里的promise 再包一层,默认都是返回resolve,再对请求返回reject信息进行处理,通过对象的属性描述是否异常。

代码实现

封装promiseWithError方法

 
const p1 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('promise resolve 1');
  }, 1000);
});

const p2 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('promise resolve 2');
  }, 2000);
});

const p3 = new Promise((resolve, reject) => {
  setTimeout(() => {
    reject('promise reject 3');
  }, 3000);
});

Promise.all([p1, p2, p3])
  .then(res => {
    console.log('resolve:', res);
  })
  .catch(err => {
    console.log('reject:', err);
  });

// 最终输出为: promise reject 3 
 

//定义异常捕获的promise方法
async function promiseWithError(p) {
  try {
    const res = await p;
    return {
      err: 0,
      data: res
    };
  } catch(e) {
    return {
      err: 1
    }
  }
}
//先转化一次promise,让所有的promise默认都是resolve,错误信息通过对象属性描述。
Promise.all([p1, p2, p3].map(item => promiseWithError(item)))
  .then(res => {
    console.log('resolve:', res);
  })
  .catch(err => {
    console.log('reject:', err);
  });

// 最终的输出为:
// resolve: [{ err: 0, data: "promise resolve 1"}, { err: 0, data:"promise resolve 2"}, { err: 1 }]