👏 在做大文件上传时,一开始使用全量并发promise.all(),后发现文件越大,尽管做了切片,但是一次性全发出去,容易阻塞网络,最终性能比分批的要差很多。所以,后来用了控制并发,但是为什么浏览器要限制并发连接数,引发了思考。在网上找了一下,结合别人的一些思考,将其做了一个整理。
🤔 那么为什么浏览器要限制并发连接数呢?
http是封装过的tcp连接,问题转化为:为什么要对tcp连接进行并行限制呢?
1️⃣ 对操作系统端口资源考虑
PC总端口数为65536,那么一个TCP(http也是tcp)链接就占用一个端口。操作系统通常会对总端口一半开放对外请求,以防端口数量不被迅速消耗殆尽。
2️⃣ 过多并发导致频繁切换产生性能问题
一个线程对应处理一个http请求,那么如果并发数量巨大的话会导致线程频繁切换。而线程的上下文切换有时候并不是轻量级的资源。这导致得不偿失,所以请求控制器里面会产生一个链接池,以复用之前的链接。所以我们可以看作同域名下链接池最大为4~8个,如果链接池全部被使用会阻塞后面请求任务,等待有空闲链接时执行后续任务。
3️⃣ 避免同一客户端并发大量请求超过服务端的并发阈值
在服务端通常都对同一个客户端来源设置并发阀值避免恶意攻击,如果浏览器不对同一域名做并发限制可能会导致超过服务端的并发阀值被BAN掉。
4️⃣ 客户端良知机制
为了防止两个应用抢占资源时候导致强势一方无限制的获取资源导致弱势一方永远阻塞状态。
http1采用持久连接,允许在同一个tcp连接上发送多个http请求和响应,不必为每个请求都建立一个新的tcp连接,但是请求和响应都必须按顺序交替发送,一个请求的响应必须在前一个请求的响应之后发送,这样会造成队头阻塞问题,一般情况下,多个请求还是多个tcp连接。
对于http2来说,每个请求和响应被分割为帧,引入多路复用技术,使得一个tcp连接上可以同时传输多个请求和响应,请求和响应可以独立的被发送和接收,无需按顺序等待。解决了队头阻塞问题,提高并发性能。
✔附上异步并发控制代码
/**
* 关键点
* 1. new promise 一经创建,立即执行
* 2. 使用 Promise.resolve().then 可以把任务加到微任务队列,防止立即执行迭代方法
* 3. 微任务处理过程中,产生的新的微任务,会在同一事件循环内,追加到微任务队列里
* 4. 使用 race 在某个任务完成时,继续添加任务,保持任务按照最大并发数进行执行
* 5. 任务完成后,需要从 doingTasks 中移出
*/
function limit(count, array, iterateFunc) {
const tasks = []
const doingTasks = []
let i = 0
const enqueue = () => {
if (i === array.length) {
return Promise.resolve()
}
const task = Promise.resolve().then(() => iterateFunc(array[i++]))
tasks.push(task)
const doing = task.then(() => doingTasks.splice(doingTasks.indexOf(doing), 1))
doingTasks.push(doing)
const res = doingTasks.length >= count ? Promise.race(doingTasks) : Promise.resolve()
return res.then(enqueue)
};
return enqueue().then(() => Promise.all(tasks))
}
// test
const timeout = i => new Promise(resolve => setTimeout(() => resolve(i), i))
limit(2, [1000, 1000, 1000, 1000], timeout).then((res) => {
console.log(res)
})