手撕代码——实现有并发限制的 Promise 调度器
题目描述
JS 实现一个带并发限制的异步调度器 Scheduler,保证同时运行的任务最多有两个。
例如目前有 4 个任务,完成时间分别为,1000ms、500ms、300ms、400ms
那么在该调度器中的执行完成顺序应该为 2、3、1、4
分析:因为1、2先进入队列中,2完成则输出2,3进入,3完成输出3,此时为800ms,4进入后的200ms,1完成输出1,而后4完成输出 4。
如何去模拟一个任务
首先,我们需要理解任务到底是什么?
现实中,任务就是被委派的事情,需要我们在指定的时间去完成,完成这个任务也需要一段时间。在 JavaScript
中,任务实际上是一段代码块,可以是同步的也可以是异步的。
我们可以用一个函数去模拟一个任务,在指定的时间去调用这个函数其实就是在指定的时间去完成这个任务。
完成任务是有时长的,那么我们怎么去模拟这个时长呢?
可以使用定时器,在计时结束后调用一个回调函数(任务)就相当于完成了这个任务,其实这个模拟挺粗糙的,因为在这个计时的过程中,我们什么也没干,任务其实在计时结束后才开始执行的,不过这并不影响我们做题。
任务其实就是一个函数,我们调用它就相当于任务开始执行,可以用代码实现一个任务创建器:
/**
* 任务创建器
* @param {Function} callback 回调函数(任务)
* @param {number} duration 任务执行时长
* @returns {() => Promise<void>}
*/
function createTask(callback, duration) {
return () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
callback();
resolve();
}, duration);
});
};
}
// 创建一个任务
const task = createTask(() => {
console.log("任务1");
}, 2000);
// 执行任务,任务时长: 2s
task()
如何理解 JS 的并发
都说 JavaScript 是单线程的,怎么会有并发呢?
JS 执行确实是单线程的,位于JS 引擎线程,但是在 JS 运转的时候,不单单只有 JS 引擎线程参与,还有诸如计时器触发线程、Http 异步请求线程等等也参与其中,浏览器是一个多进程多线程模型。
总结一下:JS 本身并没有并发行为,但是计时器、异步请求等等不属于JS 引擎线程管理,它们有单独的线程来管理,产生了并发行为。
就拿定时器来举例子,计时器的计时行为就是并发的。
当我们启动一个定时器,会交由专门的计时器触发线程来计时,时间一到,就把回调函数任务推到消息队列中。
代码实现
回到题目,我们应该怎么去实现这个有并发限制 Promise 调度器?
首先 Scheduler
是一个类,new Scheduler(2)
相当于实例化一个调度器,参数 2 是任务的最大并行数。
Scheduler
有如下属性和方法,我们一一介绍它们的作用:
-
属性:
- limit:最大并行任务数
- running:当前运行的任务数
- queue:任务队列
-
方法:
- createTask:创建一个任务,参数为一个回调函数、任务执行时长,返回值为一个函数
- addTask:添加一个任务放到队列里
- start:启动,开始处理队列里的任务
- schedule:调度任务,从队列里取一个任务出来执行
最终代码实现:
class Scheduler {
constructor(limit) {
this.limit = limit; // 最大并行任务数
this.running = 0; // 当前运行的任务数
this.queue = []; // 任务队列,每一个任务都是一个函数
}
// 创建一个任务
createTask(callback, duration) {
return () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
callback();
resolve();
}, duration);
});
};
}
/**
* 添加一个任务
* @param {Function} callback 任务,一个函数
* @param {number} duration 任务的运行时长,使用定时器模拟
* @returns void
*/
addTask(callback, duration) {
const task = this.createTask(callback, duration);
this.queue.push(task);
}
// 启动,开始处理队列里的任务
start() {
for (let i = 0; i < this.limit; i++) {
this.schedule();
}
}
// 调度任务
schedule() {
// 当任务队列为空时或者目前并发执行的任务 >= limit 时,停止任务调度
if (this.queue.length === 0 || this.running >= this.limit) {
return;
}
this.running++;
const task = this.queue.shift();
task().then(() => {
this.running--;
this.schedule();
});
}
}
测试结果
测试用例:
// 实例化一个调度器
const scheduler = new Scheduler(2);
// 添加任务
scheduler.addTask(() => {
console.log("任务1");
}, 1000);
scheduler.addTask(() => {
console.log("任务2");
}, 500);
scheduler.addTask(() => {
console.log("任务3");
}, 300);
scheduler.addTask(() => {
console.log("任务4");
}, 400);
// 任务执行
scheduler.start();
分析下运行结果:
- 初始时,队列里有 4 个任务,首先取出 **任务1 **和 任务2 ,任务1 和 任务2并发执行。
500 ms
时,任务2执行完毕,打印 "任务2",同时任务3 开始执行,任务1 和 任务3 并发执行。800 ms
时,任务3执行完毕,打印 "任务3",同时任务4 开始执行,任务1 和 任务4 并发执行。1000 ms
时,任务1执行完毕,打印"任务1",此时,任务队列已没有新任务,不需要执行新的任务,只有任务4还在执行。1200 ms
时,任务4 执行完毕,打印"任务4",此时,全部任务执行完毕。
思考
总的来说,我们模拟的这个并发任务的调度器,还是比较粗糙的,需要完善的还很多,可以从以下几个方面思考:
- 任务执行异常该怎么处理?
- 如何给任务添加优先级?
文章到这就结束了,有什么问题可以在评论区聊聊~
转载自:https://juejin.cn/post/7271103274063757369