1. 什么是并发
在我们接触计算机中的线程时会遇到个名词:并发。学过操作系统的朋友对这个哥们可能并不陌生,我们在聊如何控制并发之前,先来聊一聊并发是什么?
并发:指在同一时刻只能有一条指令执行,但多个进程指令被快速的轮换执行,使得在宏观上具有多个进程同时执行的效果,但在微观上并不是同时执行的,只是把时间分成若干段,使多个进程快速交替的执行。
上面的话可能太官方了,接下来我们用一个简单比喻来形容一下:比如你现在有两瓶饮料,现在我要你用喝一瓶的时间去喝完两瓶,这个时候你可以往两瓶里面分别插一根吸管,然后一起吸饮料,这样就能同时喝两瓶饮料了。
控制并发应用场景
我们在简单了解了并发是什么东西后,我们可以知道并发这个东西可以提高执行效率,但是天下并没有免费的午餐。我们可以想象一个场景,如果前端在很短的时间内发送几十个请求的话,那就可能会让服务器过载或者客户端性能下降,毕竟机器也不是说无上限的,这个时候我们就可以控制一段时间内的并发数量从而缓解服务器的压力。
2. 实现控制并发
在上文中我们谈到了控制并发用在什么时候,接下来我们用js来简单模拟一下控制并发。
思路:我们需要有一个任务队列来存放需要完成的任务,并且能够自己来控制一段时间内能够并发的任务个数。当并发任务个数满了的时候,如果其中有一个任务完成,那么就将其从并发的任务队列中拿出并且从等待的任务队列中取一个新的任务放入到并发任务中去。
任务的创建
首先我们先给一个函数ajax(time)
,这个函数我们将其当成需要耗费时间执行的进程。除此之外还有一个函数addTask(time, name)
用来将任务添加到任务队列中:
js
// 参数用来模拟任务执行时间
function ajax(time) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve();
}, time);
});
}
// time:任务执行时间,name:哪个任务
function addTask(time, name) {
limit
.add(() => ajax(time))
.then(() => {
console.log(`任务 ${name} 完成`);
})
}
控制并发数
接下来我们来使用class
来实现一下控制并发这个功能,在这个类中我们需要完成以下几个功能:
- 拥有任务队列,并且能控制并发任务个数
- 可以将任务添加到任务队列
- 可以执行任务队列中的任务
js
class Limit {
// 默认控制 2 个并发
constructor(paralleCount = 2) {
this.tasks = [] // 装所有任务的任务队列
this.runningCount = 0 // 正在运行任务个数
this.paralleCount = paralleCount // 并发量
}
add(task) {} // 添加任务
_run() {} // 执行任务
}
const limit = new Limit(2); // 同时2个并发
添加到任务队列
接下来我们来完善一下Limit
中add
函数的逻辑,在这个函数中我们需要完成以下几点:
- 返回一个promise对象,方便后续
then()
的执行- 每次执行都会将任务添加到任务队列,然后执行该任务
js
add(task) { // 添加任务
return new Promise((resolve, reject) => {
this.tasks.push({
task,
resolve,
reject
})
this._run()
})
}
执行任务
在这个类中的_run()
函数我们用来执行任务队列中的任务,接下来我们来简单聊聊它的几个注意点:
- 如果并发量没有达到上限那么就可以从任务队列中取出任务执行
- 如果达到了并发量,那就要等到正在并发中的某一个或者所有任务执行完成后再从任务队列中取出新的任务放入执行
- 解构出来的resolve是add函数return出来的promise的
tip:当我们有任务完成之后,就需要从等待任务队列中再拿一个任务出来执行,这时候还是重复执行_run()
,这时候我们可以用递归。
js
_run() { // 执行任务
while (this.runningCount < this.paralleCount && this.tasks.length) {
const { task, resolve, reject } = this.tasks.shift() // 从等待任务队列头部拿出任务执行
this.runningCount++
task().then(resolve, reject).finally(() => { // 一个任务完毕了最终都会走下面的代码
this.runningCount--
this._run() // 并发任务结束了后,将等待的任务队列中的任务取出执行
})
}
}
完整代码
js
function ajax(time) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve();
}, time);
});
}
// 任务队列中存储的任务
// [
// {
// () => ajax(time),
// resolve,
// reject
// },
// ]
class Limit {
// 控制并发数量默认为2
constructor(paralleCount = 2) {
this.tasks = [] // 装所有任务
this.runningCount = 0 // 正在运行任务个数
this.paralleCount = paralleCount // 并发量
}
add(task) { // 添加任务
return new Promise((resolve, reject) => {
this.tasks.push({
task,
resolve,
reject
})
this._run()
})
}
_run() { // 执行任务
while (this.runningCount < this.paralleCount && this.tasks.length) {
const { task, resolve, reject } = this.tasks.shift() // 从头部取值
this.runningCount++
task().then(resolve, reject).finally(() => { // 一个任务完毕了最终都会走下面的代码
this.runningCount--
this._run()
})
}
}
}
const limit = new Limit(2); // 同时2个并发
function addTask(time, name) {
limit
.add(() => ajax(time))
.then(() => {
console.log(`任务 ${name} 完成`);
}).catch(() => {
console.log(`任务 ${name} 失败`);
});
}
addTask(10000, 1);
addTask(5000, 2);
addTask(1000, 3);
addTask(3000, 4);
addTask(7000, 5);
执行过程
3. setTimeout
在上面的代码中,有同学可能会有疑惑setTimeout不是放在宏任务队列中吗?为什么会两个setTimeout一起开始计时并且执行呢?
在浏览器中,所有的计时器全部归浏览器去处理,所以在浏览器眼里,不管有多少个计时器,都是走同一套倒计时。定时器确实都是要放在宏任务队列中,当浏览器倒计时到了某一个点的时候,只要V8引擎的主线程现在是空着的,它就会把宏任务队列当中的定时器拿出来执行倒计时,到另外一个点的时候,如果此时引擎还是空的,那么它就会把另一个定时器拿出来执行。
总结
- 保证所有的任务都受控制(给任务包裹一层函数)
- 创建添加任务的 add 函数,并将 add 的 resolve,reject同任务一起存放
- 创建执行任务的 run 函数,在每个任务执行完毕后,执行 add 函数的resolve,reject,最后递归执行 run 函数