最近在看 react 的调度过程,其中涉及到了优先级调度,任务的过期时间又会提高任务的优先级。调度过程分为调度者和实际执行者。调度的任务队列也分成两个部分,一个是未过期的任务,一个是已经过期的任务。调度者会优先调度过期时间最长的任务。等等
之前在大学也学过调度算法,老师也要求写过,那会写得很慢,写得很艰难。看到 react 将调度算法分成调度者和实际执行者,就很受启发。准备动手再实现一个调度算法。算是给大学作业画上一个句号,看看这次写得是不是好很多。当然下面的调度实现和 react 中的调度过程完全不一样,不能当作理解 react 源码的入口
这篇文章将会实现 FIFO
先进先出,SJF
短作业优先,SRNT
最短剩余时间优先。最后一个作业是抢占式的。
创建 task 对象
javascript
class Task {
name = "";
arrivalTime = 0;
totalTime = 0;
executeTime = 0;
waitTime = 0;
leftTime = 0;
constructor(arrivalTime, totalTime, name) {
this.arrivalTime = arrivalTime;
this.totalTime = totalTime;
this.leftTime = this.totalTime;
this.name = name || "Task" + Math.random();
}
execute() {
console.log(this.name, " working... left time is ", --this.leftTime);
this.executeTime++;
if (this.leftTime == 0) {
console.log(this.name, " has finished. ");
}
}
}
首先创建一个 Task 对象,对象中有很多属性:
- name:任务名称,默认为随机生成的名称。
- arrivalTime:任务到达时间。
- totalTime:任务的总执行时间。
- executeTime:任务已执行的时间。
- waitTime:任务等待时间(在队列中等待被调度执行的时间)。
- leftTime:任务剩余需要执行的时间。
- finishTime:任务完成时间。
- constructor:构造函数,用于初始化任务的属性。
- arrivalTime:任务到达时间,默认为 0。
- totalTime:任务的总执行时间,默认为 0。
- name:任务名称,默认为 "Task" + 随机数。
还有一个 execute 方法,"模拟"任务的实际执行。
- 首先,它将任务剩余时间减 1。然后,它将已执行时间加 1。输出任务正在执行的消息,并打印剩余时间。如果任务剩余时间变为 0,则表示任务已完成,输出任务完成的消息。
创建调度者和执行者
javascript
class schedule {
constructor(taskInfo) {
this.time = 0; // 当前时间
this.executeTask = null; // 当前正在执行的任务
this.waitTaskQueue = []; // 等待执行的任务队列
this.pendingTaskQueue = []; // 已添加但尚未到达执行时间的任务队列
this.finishTask = []; // 已完成的任务队列
this.executer = new Executer(this); // 执行任务的对象
this.id = null; // 定时器 ID,用于定期执行任务
this.lastTaskIndex = 0; // 上次添加任务时的索引
}
addTask(task) {
this.waitTaskQueue.push(task); // 将任务添加到等待执行的任务队列中
}
startSchedule() {
this.id = setInterval(() => {
this.execute(); // 执行任务
this.time++; // 更新当前时间
}, 1000); // 每隔 1000 毫秒(1 秒)执行一次
}
execute() {
// 判断是否还有任务进来
this.judgeNewTask();
// 判断任务是否执行完成
if (this.executeTask == null && this.waitTaskQueue.length == 0 && this.taskInfo.length == 0) {
clearInterval(this.id); // 如果没有任务,则关闭定时器
return;
}
// 判断当前任务是否执行完成,如果执行完成,就上新任务,如果没有,就跳过
if (this.executeTask == null) {
// 获取最前面的进程,即先进先出
if (this.waitTaskQueue.length) this.executeTask = this.waitTaskQueue.shift();
}
if (this.executeTask) this.executer.execute();
}
judgeNewTask() {
let addedTaskIndex = [];
for (let i = 0; i < this.taskInfo.length; i++) {
if (this.taskInfo[i].arrivalTime <= this.time) {
const { arrivalTime, totalTime, name } = this.taskInfo[i];
this.addTask(new Task(arrivalTime, totalTime, name));
addedTaskIndex.push(i);
this.lastTaskIndex++;
} else {
break;
}
}
//delete tasks been added
this.taskInfo = this.taskInfo.filter((item, index) => {
return addedTaskIndex.includes(index) == false;
});
}
}
class Executer {
constructor(schedule) {
this.schedule = schedule; // 调度对象
}
execute() {
this.schedule.executeTask.execute(); // 执行任务
// 检查任务是否执行完成,如果完成就将任务删除
if (this.schedule.executeTask.leftTime == 0) {
this.schedule.finishTask.push(this.schedule.executeTask); // 将任务添加到已完成任务队列中
this.schedule.executeTask = null; // 将当前任务设置为 null
}
}
}
上面代码定义了两个对象,分别是schedule 调度者,Executer 执行者。调度者schedule
主要任务是维护进程的就绪队列和执行状态,执行者Executer
的主要任务就是执行进程。
调度者schedule
每一个时间周期都会检查是否有新进程进程,并且检查当前处于执行状态的进程是否为空,如果为空就调度新的进程置于执行状态。如果不为空,就继续调度执行者Executer
执行进程
代码中利用 setInterval
方法来模拟 CPU 的循环执行调度者schedule
代码,并且每个时间周期的长度为 1 秒钟。同时用 time
模拟当前的时钟。所有进程的时间记录都基于这个变量
执行者Executer
在执行任务之后,会检查任务是否执行完成,如果执行完成,就将任务放到finishTask
,并且将当前的executeTask
置为 null,表示现在没有正在执行的进程
执行函数
javascript
const taskInfo = [
{ name: "A", arrivalTime: 0, totalTime: 3 },
{ name: "B", arrivalTime: 1, totalTime: 1 },
{ name: "C", arrivalTime: 3, totalTime: 3 },
{ name: "D", arrivalTime: 5, totalTime: 1 },
];
new schedule(taskInfo).startSchedule();
运行 schedule
的时候,会传入一个进程表
这样的数据,其中存放着需要运行的进程基本信息,有 name,到达时间arrivalTime
,执行时间totalTime
。
schedule
在每个运行周期,都会去读取进程表,将进程表中的arrivalTime
与当前的 time
变量进行比较,如果小于 time
,就说明进程已经到达,需要放进就绪队列waitTaskQueue
,如果大于 time
,说明进程还没到达,就不做任何处理。
代码写得很简单,只是其中的逻辑需要多多理解,就不做多解释了,下面看看实际执行的输出
css
A working... left time is 2
A working... left time is 1
A working... left time is 0
A has finished.
B working... left time is 0
B has finished.
C working... left time is 2
C working... left time is 1
C working... left time is 0
C has finished.
D working... left time is 0
D has finished.
可以看到进程实际运行过程,4 个进程运行过程是 A,B,C,D. 完全按照进程到达的顺序来调度。
决定调度顺序的代码就是:
javascript
// 判断当前任务是否执行完成,如果执行完成,就上新任务,如果没有,就跳过
if (this.executeTask == null) {
// 获取最前面的进程,即先进先出
if (this.waitTaskQueue.length) this.executeTask = this.waitTaskQueue.shift();
}
每当当前任务执行完成,就会从就绪队列的队头获取新的 task ,放到executeTask
上面。因为数据就是按照到达时间排列的。
这就实现了FIFO
先进先出的调度算法。
SJF Short Job First
如何实现短作业优先,相信大家已经知道了,就是修改那段决定调度顺序的代码就可以了。
javascript
execute() {
//判断是否还有任务进来
this.judgeNewTask();
//判断任务是否执行完成
if (this.executeTask == null && this.waitTaskQueue.length == 0 && this.taskInfo.length == 0) {
clearInterval(this.id);
return;
}
//判断当前任务是否执行完成,如果执行完成,就上新任务,如果没有,就跳过
this.SJF();
if (this.executeTask) this.executer.execute();
}
FIFO() {
if (this.executeTask !== null) return;
if (this.waitTaskQueue.length) this.executeTask = this.waitTaskQueue.shift();
}
SJF() {
if (this.executeTask !== null) return;
if (this.waitTaskQueue.length) {
let index = 0;
for (let i in this.waitTaskQueue) {
if (this.waitTaskQueue[index].totalTime > this.waitTaskQueue[i].totalTime) {
index = i;
}
}
this.executeTask = this.waitTaskQueue[index];
this.waitTaskQueue.splice(index, 1);
}
}
将代码提取出来放在方法里面。终点来看SJF
方法,其中选择新 task 的标准是执行时间,谁的执行时间短,谁就先开始调度
实际输出:
javascript
const taskInfo = [
{ name: "A", arrivalTime: 0, totalTime: 3 },
{ name: "B", arrivalTime: 1, totalTime: 2 },
{ name: "C", arrivalTime: 3, totalTime: 1 },
{ name: "D", arrivalTime: 5, totalTime: 1 },
];
new schedule(taskInfo).startSchedule();
调整下输入的数据,可以看到 B 比 C 先到,但是 C 的执行时间比 B 要短。按照 FIFO 的逻辑,B 会比 C 先得到执行。但是按照 SJF,C 会比 B 先执行。
执行结果:
latex
A working... left time is 2
A working... left time is 1
A working... left time is 0
A has finished.
C working... left time is 0
C has finished.
B working... left time is 1
B working... left time is 0
B has finished.
D working... left time is 0
D has finished.
SRTN shortest remaining
依旧是修改那部分代码
javascript
class schedule{
//...
// 省略其他代码
execute() {
//判断是否还有任务进来
this.judgeNewTask();
//判断任务是否执行完成
if (this.executeTask == null && this.waitTaskQueue.length == 0 && this.taskInfo.length == 0) {
clearInterval(this.id);
return;
}
//判断当前任务是否执行完成,如果执行完成,就上新任务,如果没有,就跳过
this.SRTN();
if (this.executeTask) this.executer.execute();
}
// 定义 SRTN 方法
SRTN() {
// 如果等待任务队列不为空
if (this.waitTaskQueue.length) {
// 初始化索引为 0
let index = 0;
// 遍历等待任务队列,找到剩余时间最短的任务
for (let i in this.waitTaskQueue) {
if (this.waitTaskQueue[index].leftTime > this.waitTaskQueue[i].leftTime) {
// 更新索引为当前任务的位置
index = i;
}
}
// 如果执行任务存在且剩余时间大于等待任务队列中最短任务的剩余时间
if (this.executeTask !== null && this.executeTask.leftTime > this.waitTaskQueue[index].leftTime) {
// 移除等待任务队列中最短任务,并将其添加到执行任务队列中
const newTask = this.waitTaskQueue[index];
this.waitTaskQueue.splice(index, 1);
this.waitTaskQueue.push(this.executeTask);
// 更新执行任务为新的任务
this.executeTask = newTask;
}
// 如果执行任务为空,直接将等待任务队列中最长任务设置为执行任务
if (this.executeTask == null) {
const newTask = this.waitTaskQueue[index];
this.waitTaskQueue.splice(index, 1);
this.executeTask = newTask;
}
}
}
//省略其他代码
}
简单介绍一个过程
因为SRTN
是抢占式的,所以无论当前是否有任务执行,都去找一个剩余时间最短的任务,记作 Task A。找到之后,如果当前有执行态的任务,且当前任务的剩余时间比 Task A 还要长, 就将当前任务放回就绪队列,然后把 Task A 放到executeTask
上。
如果当前没有执行态的任务,就直接把 Task A 放到executeTask
上
执行代码
javascript
const taskInfo = [
{ name: "A", arrivalTime: 0, totalTime: 3 },
{ name: "B", arrivalTime: 1, totalTime: 1 },
{ name: "C", arrivalTime: 3, totalTime: 3 },
{ name: "D", arrivalTime: 5, totalTime: 1 },
];
new schedule(taskInfo).startSchedule();
// A working... left time is 2
// B working... left time is 0
// B has finished.
// A working... left time is 1
// A working... left time is 0
// A has finished.
// C working... left time is 2
// D working... left time is 0
// D has finished.
// C working... left time is 1
// C working... left time is 0
// C has finished.
很明显的抢占。
进程 B 来了,A 还没有执行完,但是 B 的剩余时间是 1,而 A 的剩余时间是 2。所以先执行 B。D 抢占 C 的过程也是一样的
总结
这篇文章介绍了进程调度算法中的 FIFO
先进先出,SJF
短作业优先,SRNT
最短剩余时间优先。代码清晰,注释详细,是一篇不可多的的好文章啊
下篇文章会继续分享调度算法中的时间片轮转调度,和优先级调度算法。
喜欢就关注一下吧❤️,你的点赞和关注是我不断分享的动力,爱你们