1

Jan-21-2022 12-13-30.gif

起因

项目需要做一个上传功能,每次上传20个视频,不处理直接传的话会有一个阻塞问题。
因为一个网页最多同个域名只能有6个tcp连接。不控制并发同时传6个,但是用户后续的所有调其他接口的操作都会被挂起来。网络不好的情况下更是灾难,所以需要控制并发,一次最多只传3个。
正好又有这种类型的面试题,写个demo讲下原理。

后端代码

后端需要有基本的返回,随机延迟,随机错误,能跨域

const http = require('http')
http.createServer((req,res)=>{
    
    const delay = parseInt(Math.random()*5000)
    console.log(delay);
    setTimeout(()=>{
        const returnerror = Math.random()>0.8
        // if(returnerror){
        //     res.writeHead(500,{
        //         // 'Content-Type': 'text/plain',
        //         'Access-Control-Allow-Origin':'*',
        //         'Access-Control-Allow-Headers':'Content-Type, Content-Length, Authorization, Accept, X-Requested-With , yourHeaderFeild',
        //         'Access-Control-Allow-Methods':'GET',
        //     }).end('error,from '+req.url+' after '+delay+'ms')
        // }else{
            res.writeHead(200,{
                // 'Content-Type': 'text/plain',
                'Access-Control-Allow-Origin':'*',
                'Access-Control-Allow-Headers':'Content-Type, Content-Length, Authorization, Accept, X-Requested-With , yourHeaderFeild',
                'Access-Control-Allow-Methods':'GET',
            }).end('OK,from '+req.url+' after '+delay+'ms')
        // }
        
    },delay)   
}).listen(3000,()=>{
    console.log('服务启动在3000!');
})

前端代码

html主要为了动画的一个展示,就不细说了。
js分为两部分,一部分控制dom渲染具体看总结出给的源码地址,这个也不细说了,第二部分就是并发控制器
input数组里面是请求地址

const input = new Array(10).fill('http://localhost:3000/').map((item,index)=>`${item}${index+1}`)

image.png

基础版

并发控制主要是两个函数和一个promise队列

  • 一个负责不断的递归往队列里面加promise。
  • 另外一个负责生产promise,处理异步请求逻辑
function handleFetchQueue(input, max) {
        const requestsQueue = []; // 请求队列
        addReactive(requestsQueue) //对requestsQueue添加响应式,重写push,splice方法使其能同时改变页面上的队列dom。
        let i = 0;
        const req = (i)=>{  //产生一个promise请求 成功则删除队里中promise 再添加一个请求
            return  fetch(input[i]).then(res=>res.text()).then(res=>{
                addEl(res) //结果渲染页面列表
                const index = requestsQueue.findIndex(item=>item===req)
                requestsQueue.splice(index,1)
                checkAddReq()
            })
        }
        const checkAddReq = ()=>{
            if(i>=input.length) return // 请求数不能越界
            if(requestsQueue.length+1 <= max) { // 并发不能越界
                setTimeout(()=>{ //加延迟为了提高动画效果,实际不需要
                    requestsQueue.push(req(i++))
                    checkAddReq()
                },50)
                
            }
        }   
       checkAddReq();
    }   
    handleFetchQueue(input,3)

处理异常

需要考虑的问题是异常了怎么办。
异常对于并发控制也就是计数原理应该没有区别,只是需要额外再promise生成这里添加下业务逻辑即可。
我们把后端代码随机错误的注释打开
然后前端核心逻辑改一下

const req = (i)=>{  
            return  fetch(input[i]).then(async(res)=>{
                if(res.status===500){ //这里异步改一下,得让走catch
                    const text = await res.text()
                    throw new Error(text);
                }else{
                    return res.text()
                }
            }).then(res=>addEl(res),error=>addEl(error,true))
              .finally(()=>{ //无论成功失败并发逻辑一样,只是then中业务逻辑不同
                const index = requestsQueue.findIndex(item=>item===req)
                requestsQueue.splice(index,1)
                checkAddReq()
            })
        }

总结

完整地址 https://github.com/fyy92/code...

  • index.html 不考虑异步
  • index1.html 考虑异常
  • serve.js 后端代码

总结一下

  • 并发控制关键在两个函数,一个控制生成业务Promise,一个迭代控制并发队列添加业务Promise。
  • 对于异常,需要保证并发逻辑一样,不同的是业务逻辑的处理。

补充

最近发现一个库 asyncpool 仿写一下它

const timeout = i => new Promise(resolve => setTimeout(() => resolve(i), i));
const results = await asyncPool(2, [1000, 5000, 3000, 2000], timeout);
// Call iterator (i = 1000)
// Call iterator (i = 5000)
// Pool limit of 2 reached, wait for the quicker one to complete...
// 1000 finishes      1秒后
// Call iterator (i = 3000)
// Pool limit of 2 reached, wait for the quicker one to complete...
// 3000 finishes      4秒后
// Call iterator (i = 2000)
// Itaration is complete, wait until running ones complete...
// 5000 finishes      5秒后
// 2000 finishes      6秒后
// Resolves, results are passed in given array order `[1000, 5000, 3000, 2000]`.

网上的版本一般用Promise.settle Promise.all
所以比如生成一个完整的Promse数组
而我们前面介绍的是分阶段去产生Promise
所以晚上的相当于把我们生成promise的操作再封一层函数,然后就可以用Promise.settle Promise.all。
我们这里直接使用Promise.settle Promise.all原理还是按之前的思路去做

unction asyncPool(poolLimit, array, iteratorFn){ 
   
    return new Promise((resolve,reject)=>{
        const queue =[]
        const result = []
        let num = 0
        let i = 0
        const add = ()=>{
            // console.log(i,queue.length)
            if(i>=array.length) return 
            if(queue.length>=poolLimit) return 
            const p = iteratorFn(array[i]).then((res)=>{ //这里最好用finally 但是注意finally res为undefined 
                // console.log(i)
                result.push(res)
                queue.splice(queue.findIndex(i=>i===p),1)
                add(i) //i已经加过了被return了的
                num++
                console.log(num)
                if(num===array.length){
                    resolve(result)
                }
            })
            queue.push(p)
            add(++i)
        }
        add(0)
    })
}


let oldtime = new Date().getTime()
const timeout = i => new Promise(resolve => setTimeout(() => {
    resolve(i)
    // console.log(i,new Date().getTime()-oldtime)
}, i));


asyncPool(2, [1000, 5000, 3000, 2000], timeout).then(res=>{
    console.log(res) //1000 3000 5000 2000
})

Runningfyy
1.3k 声望661 粉丝