开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第1天,点击查看活动详情
想知道如何实现一个简单的限制并发数量,首先我们需要知道以下几点
JS如何执行并发请求
如我们所知,JS是单线程的,其工作原理是将每一个任务切分成多个片段然后交给v8引擎去快速交替按照顺序执行这些片段。
一般情况下,在单线程中,所有的任务队列是需要排队,前一个任务执行完毕才会执行下一个,但如果前一个任务执行的时间很长,后面的任务就一直被阻塞一直等待着吗?所以这个时候我们的JS异步就显得尤为重要了。
那么JS是如何实现异步请求的呢?或者说并发请求的? 答案就是消息队列和事件循环啦!!
这里可能又有人要问了,异步就是并发吗?那我们简单解释一下
什么是异步?
所谓异步是指你不需要等待函数调用结果返回, 调用之后可以马上去做另外一件事
什么是并发?
并发是指有两个任务A和B,在一段时间内,通过在任务间切换来完成两个任务,这种情况叫并发。
即可以不同步地执行多个任务, 所谓同步就是执行完这件事, 再处理另外一件事
**可见并发和异步某种程度上有相似的性质, 这是因为异步其实就是并发的一种方式
那么我们再提一嘴和并发经常一起被问区别的并行
什么是并行
和并发不同的是并行是一个微观概念,假设我们的CPU有两个核心,那么我们就可以同时完成A,B两个任务。即可以在同一时间段同时完成多个任务的情况可以称为并行
我们再来简单说说上面的提到的消息队列和事件循环,当然这次我们的重点不是他们,就一句话概括啦~
消息队列
我们知道队列是一个先进先出的结构,而这个队列里面存入的一般是我们上面提到的JS的任务
事件循环
主线程不断重复的从消息队列中取出消息执行,当消息队列为空时,就会等待消息队列中有消息的存在,而主线程只有将当前消息执行完毕后,才会去执行下一个消息。这个过程就叫做事件循环。
代码实现
了解了上面的概念之后,我们就要逐步用代码实现啦
我们首先选择用一个类来实现,那么既然是限制并发,那么构造函数里面必有得一个参数就是limit限制参数,其次我们需要一个队列,来存储我们获取到得任务,这个就定义一个数组来实现啦。
class Scheduler {
constructor(limit) {
this.limit = limit;//限制并发数量
this.queue = [];//存储消息的消息队列
}
……
}
想一想接下来该干什么,我们既然有了可以存储消息的消息队列了,我们是不是该实现一个消息存储的方法了?
我们借助定时器来帮我们模拟执行时间不同的异步请求,同时为确保我们的是执行完当前任务再执行下一个任务,我们这里需要借助Promise来帮我们来包裹一下异步请求,使我们的返回结果是一个Promise对象,可以调用then
add(time, order) {
//模拟不同的异步请求任务
const foo = () => {
return new Promise((reslove, reject) => {
setTimeout(() => {
console.log(order);//注意!!order是为了帮我们查看执行完毕的是插入的第几个任务
reslove();
}, time);
});
};
//向队列中插入任务
this.queue.push(foo);
}
有了任务队列和任务存储逻辑了,接下来就是事件循环做任务的拿取和执行啦,既然我们要限制并发的数量,那么limit这个参数肯定是要用到的,那么如何控制一次只获取limit个数量的任务呢?那最直接的方式就是循环啦!
taskStart() {
for (let i = 0; i < this.limit; i++) {
……
}
}
这个事件循环并不完整,让我们来思考两个问题
- 循环里面该做什么呢?我们一次循环了两个异步任务,但并未获取出来执行。
- 我们两个任务的执行时间完全不同,什么时候进行下一个任务的执行呢?当然是先执行完毕的那个异步任务先获取执行队列中下一个任务啊!
显然这一套逻辑在for循环中实现是不可能的,我们需要另外封装一个请求函数,来帮助我们实行
taskStart() {
for (let i = 0; i < this.limit; i++) {
this.request();//两次循环都去到执行任务的逻辑中
}
}
//请求队列中异步消息执行的函数
request() {
if (!this.queue.length) return;//如果消息队列中一个任务都没有了就返回
let item = this.queue.shift();//从头部拿取第一个任务,先进先出的原则
item().then(() => {//先执行完的任务,再调取任务队列中下一个任务
this.request();
});
}
}
至此,我们的scheduler类实现完毕了
接下来就是进行调试,创建实例化对象,调用里面的方法看看能不能行
let scheduler = new Scheduler(2);//创建一个实例化对象传入一个参数,限制一次执行2个任务
//定义一个调用插入任务队列的函数,省去了反复写scheduler.add这么一大坨调用示例的麻烦
const addTask = (time, order) => {
scheduler.add(time, order);
};
addTask(1000, "1");//同等于scheduler.add(1000, "1");
addTask(500, "2");
addTask(300, "3");
addTask(400, "4");
scheduler.taskStart();
看看结果:
我们来算一算是否对的上 我们先插入队列的是order为1、2 的两个任务,当然就是这两个先执行
addTask(1000, "1")
addTask(500, "2");
1的执行时间为1000秒后,而2的执行时间为500毫秒,故先打印2
2执行完毕后立即去获取执行3,addTask(300, "3"),3在300毫秒后执行,500+300=800还是小于1的1000毫秒,所以再打印3
3执行完毕立马又去获取执行任务4,addTask(400, "4"),4在400毫秒后执行,这个时候事件累加成了500+300+400=1200毫秒,1200毫秒大于1000毫秒,所以1应该先执行完毕了,先打印1,再打印4,答案没错!我们成功啦!
以下附上完整代码
class Scheduler {
constructor(limit) {
this.limit = limit;
this.queue = [];
}
add(time, order) {
const foo = () => {
return new Promise((reslove, reject) => {
setTimeout(() => {
console.log(order);
reslove();
}, time);
});
};
this.queue.push(foo);
}
taskStart() {
for (let i = 0; i < this.limit; i++) {
this.request();
}
}
request() {
if (!this.queue.length) return;
let item = this.queue.shift();
item().then(() => {
this.request();
});
}
}
let scheduler = new Scheduler(2);
const addTask = (time, order) => {
scheduler.add(time, order);
};
addTask(1000, "1");
addTask(500, "2");
addTask(300, "3");
addTask(400, "4");
scheduler.taskStart();
// 2314