本文发布且更新于个人博客 https://www.xerrors.fun/JobSchedulingProgram/
我相信一个好的算法应该是优美的,不需要过多的解释,代码前后自成一体;这也是我向往的目标,不过从目前的情况来看,我还是需要努力的。
这次的作业是「单道处理系统的作业调度模拟程序」要求实现 3 种作业调度算法:
- 先来先服务(FCFS)
- 最短作业优先(SJF)
- 响应比高者优先(HRRN)
对每种调度算法都要求打印每个作业开始运行时刻、完成时刻、周转时间、带权周转时间,以及这组作业的平均周转时间及带权平均周转时间,以比较各种算法的优缺点。
三种调度算法的原理
假设咱们学生就是一个「单道处理系统」(谦虚),当面对很多科作业到来的时候,有以下几种应对办法
FCFS 先来先服务:先布置的作业优先做,所有作业排好队,后来的作业放在最后面;
最短作业优先:哪个省事先做哪个,所有作业按照要花费的时间排成一个序列;
响应比高者优先:如果哪个先来先做哪个,可能导致比较紧急且简单的工作没法完成,比如老师让当堂交 3 道选择题,这推到后面是不合适的;如果使用最短作业优先呢?可能刚一开始留下的一个比较繁琐的任务一直没法做;比如说暑假开始了,你要准备学车,但是又想出去玩,又想打游戏,等到暑假结束,还是没去学车(亲身经历);所以要综合考虑等待时间以及处理的时间开销。
Python 实现
JCB 的数据结构
每个作业由一个作业控制块JCB表示,JCB可以包含如下信息:作业名、提交时间、所需的运行时间、所需的资源、作业状态、链指针等等;所以最终的 JCB 类就直接创建出来了,千言万语不如直接看代码
class JCB:
# 表示系统中的资源数量,其中 1 表示独占资源
srcs = [3, 2, 4, 1, 1, 1]
clock = 0 # 类的时钟
def __init__(self, name, time, srcs=None):
self.name = name # 名称
self.time = time # 所需时间
self.status = 'Wait' # 作业的状态
self.commit_time = JCB.clock # 作业到达时间
# 所需的资源,单道程序调度用不到的
self.srcs = srcs if srcs else [0] * len(JCB.srcs)
self.pointer = None # 链指针,不知道要用来干嘛
self.start_time = -1 # 作业开始执行的时间,未执行为 -1
self.rp = 0 # 优先级
JCB.clock += 1 # 类的时钟 +1
# 计算优先级,越大优先级越高,越靠前
def calcRp(self, clock):
if self.start_time == -1:
self.rp = (clock - self.commit_time + self.time) / self.time
其中 calcRp
函数会根据「调度程序」当前的时间 clock
来计算该作业的优先级。
初始化 JSB
又是使用随机数生成作业;同样这次在创建数组的时候是使用了快速创建数组的方法:
a = [i for i in range(n)]
# 等同于
a = []
for i in range(n):
a.append(i)
从上面的类中可以看到,必要的参数就两个,一个是名称,一个是完成作业所需的时间;作业的到达时间我们直接使用 JSB 类的属性 clock 直接赋值。
# 初始化所有 JCB
def init():
# 先考虑单程序的情况,也就是不需要考虑资源的情况
jsb_list = [JCB(chr(ord('A') + i), random.randrange(2, 6)) for i in range(random.randrange(5, 9))]
for job in jsb_list:
print('名称:{}\t到达时间:{}\t所需时间:{}\t所需资源:{}'.format(
job.name,
job.commit_time,
job.time,
job.srcs
))
print()
return jsb_list
运行结果:
名称:A 到达时间:0 所需时间:3 所需资源:[0, 0, 0, 0, 0, 0]
名称:B 到达时间:1 所需时间:2 所需资源:[0, 0, 0, 0, 0, 0]
名称:C 到达时间:2 所需时间:5 所需资源:[0, 0, 0, 0, 0, 0]
名称:D 到达时间:3 所需时间:5 所需资源:[0, 0, 0, 0, 0, 0]
名称:E 到达时间:4 所需时间:4 所需资源:[0, 0, 0, 0, 0, 0]
名称:F 到达时间:5 所需时间:4 所需资源:[0, 0, 0, 0, 0, 0]
名称:G 到达时间:6 所需时间:4 所需资源:[0, 0, 0, 0, 0, 0]
名称:H 到达时间:7 所需时间:5 所需资源:[0, 0, 0, 0, 0, 0]
调度算法的实现
其实这三种算法的核心是一样的,只是对作业的「比较方法」不一样,也就是说,这三种算法可以使用同一个函数,只是在「初始化堆」的时候使用的比较函数是不一样的。
所以定义两个函数,一个是实现算法的核心功能 func,另外一个函数是根据调度方法的不同提供不同的比较函数 getLowerThan
:
def func(jobs, method):
JCB.__lt__ = getLowerThan(method)
...
# 下面的内容先省略
return None
def getLowerThan(method):
# 我感觉这个函数写的很丑,但是又没啥好办法
def FCFS_lt(self, other):
return self.commit_time < other.commit_time
def SJF_lt(self, other):
return self.time < other.time
def HRRN_lt(self, other):
return self.rp > other.rp
if method == 'FCFS':
return FCFS_lt
elif method == 'SJF':
return SJF_lt
elif method == 'HRRN':
return HRRN_lt
我们把类 JCB 的「小于号」进行了运算符重构,所以我们在使用 heapq.heapify(array)
的时候会调用重构的函数进行计算。
那么最后,整个算法就差最核心的算法没提到了,下面是简化之后的代码,删除了一些对周转时间的计算以及输出,完整代码见文末;因为要考虑作业的到达情况,所以整个函数里面有两个序列,一个是完整的作业序列 jobs
,当时钟 「clock」 到达某个作业的「到达时间」的时候,将这个作业从 jobs
放到 jobs_heap
里面:
def func(jobs, method):
JCB.__lt__ = getLowerThan(method)
# 初始化,需要考虑作业的到达时间
clock = 0
currentJob = None # 当前任务
jobs_heap = []
while len(jobs) != 0 or len(jobs_heap) != 0 or currentJob.status != 'Finish':
# 作业到达并入堆
while len(jobs) > 0 and clock == jobs[0].commit_time:
heapq.heappush(jobs_heap, jobs.pop(0))
# 计算优先级
if method == 'HRRN':
for i in jobs_heap:
i.calcRp(clock)
heapq.heapify(jobs_heap)
# 判断当前是否有任务或者当前任务额是否已完成
if not currentJob or currentJob.status == 'Finish':
currentJob = heapq.heappop(jobs_heap)
currentJob.start_time = clock
# 如果有任务,当前任务所需时间 - 1
currentJob.time -= 1
clock += 1
# 判断当前任务是否结束
if currentJob.time == 0:
currentJob.status = 'Finish'
return None
需要注意的是入堆那部分代码,采用高响应优先的时候,需要根据当前时间实时计算优先级,所以每一次循环(时间片)都要重新计算优先级,然后变成优先队列。
完整代码以及参考资料
import random
import heapq
import copy
class JCB:
# 表示系统中的资源数量,其中 1 表示独占资源
srcs = [3, 2, 4, 1, 1, 1]
clock = 0 # 类的时钟
def __init__(self, name, time, srcs=None):
self.name = name # 名称
self.time = time # 所需时间
self.status = 'Wait' # 作业的状态
self.commit_time = JCB.clock # 作业到达时间
# 所需的资源,单道程序调度用不到的
self.srcs = srcs if srcs else [0] * len(JCB.srcs)
self.pointer = None # 链指针,不知道要用来干嘛
self.start_time = -1 # 作业开始执行的时间,未执行为 -1
self.rp = 0 # 优先级
JCB.clock += 1 # 类的时钟 +1
# 计算优先级,越大优先级越高,越靠前
def calcRp(self, clock):
if self.start_time == -1:
self.rp = (clock - self.commit_time + self.time) / self.time
# 初始化所有 JCB
def init():
# 先考虑单程序的情况,也就是不需要考虑资源的情况
jsb_list = [JCB(chr(ord('A') + i), random.randrange(2, 6)) for i in range(random.randrange(5, 9))]
for job in jsb_list:
print('名称:{}\t到达时间:{}\t所需时间:{}\t所需资源:{}'.format(
job.name,
job.commit_time,
job.time,
job.srcs
))
print()
return jsb_list
def func(jobs, method):
JCB.__lt__ = getLowerThan(method)
# 保存作业的数量用于计算平均周转时间
job_count = len(jobs)
# 初始化,需要考虑作业的到达时间
clock = 0
currentJob = None # 当前任务
jobs_heap = []
# 总周转时间以及带权周转时间
turnover_time = 0
turnover_time_wei = 0
while len(jobs) != 0 or len(jobs_heap) != 0 or currentJob.status != 'Finish':
# 作业到达并入堆
while len(jobs) > 0 and clock == jobs[0].commit_time:
heapq.heappush(jobs_heap, jobs.pop(0))
# 计算优先级
if method == 'HRRN':
for i in jobs_heap:
i.calcRp(clock)
heapq.heapify(jobs_heap)
# 判断当前是否有任务
if not currentJob or currentJob.status == 'Finish':
currentJob = heapq.heappop(jobs_heap)
currentJob.start_time = clock
currentJob.time -= 1
clock += 1
# 如果有任务,当前任务所需时间 - 1
# 判断当前任务是否结束
if currentJob.time == 0:
# 计算周转时间
cur_turnovertime = clock - currentJob.commit_time
# 计算带权周转时间
cur_turnovertime_wei = cur_turnovertime / (clock - currentJob.start_time)
# 修改状态为完成状态
currentJob.status = 'Finish'
print('作业{}运行时间:{}\t完成时间:{}\t周转时间:{}\t带权周转时间{:.4f}'.format(
currentJob.name,
clock - currentJob.start_time,
clock,
cur_turnovertime,
cur_turnovertime_wei))
turnover_time += cur_turnovertime
turnover_time_wei += cur_turnovertime_wei
print('{} 的平均周转时间为:{:.3f}\t带权平均周转时间:{:.3f}\n'.format(
method,
turnover_time / job_count,
turnover_time_wei / job_count))
return None
def getLowerThan(method):
def FCFS_lt(self, other):
return self.commit_time < other.commit_time
def SJF_lt(self, other):
return self.time < other.time
def HRRN_lt(self, other):
return self.rp > other.rp
if method == 'FCFS':
return FCFS_lt
elif method == 'SJF':
return SJF_lt
elif method == 'HRRN':
return HRRN_lt
def main():
jobs = init()
for method in ['FCFS', 'SJF', 'HRRN']:
func(copy.deepcopy(jobs), method)
if __name__ == '__main__':
main()
运行结果:
名称:A 到达时间:0 所需时间:4 所需资源:[0, 0, 0, 0, 0, 0]
名称:B 到达时间:1 所需时间:4 所需资源:[0, 0, 0, 0, 0, 0]
名称:C 到达时间:2 所需时间:4 所需资源:[0, 0, 0, 0, 0, 0]
名称:D 到达时间:3 所需时间:3 所需资源:[0, 0, 0, 0, 0, 0]
名称:E 到达时间:4 所需时间:3 所需资源:[0, 0, 0, 0, 0, 0]
名称:F 到达时间:5 所需时间:2 所需资源:[0, 0, 0, 0, 0, 0]
名称:G 到达时间:6 所需时间:2 所需资源:[0, 0, 0, 0, 0, 0]
名称:H 到达时间:7 所需时间:2 所需资源:[0, 0, 0, 0, 0, 0]
作业A运行时间:4 完成时间:4 周转时间:4 带权周转时间1.0000
作业B运行时间:4 完成时间:8 周转时间:7 带权周转时间1.7500
作业C运行时间:4 完成时间:12 周转时间:10 带权周转时间2.5000
作业D运行时间:3 完成时间:15 周转时间:12 带权周转时间4.0000
作业E运行时间:3 完成时间:18 周转时间:14 带权周转时间4.6667
作业F运行时间:2 完成时间:20 周转时间:15 带权周转时间7.5000
作业G运行时间:2 完成时间:22 周转时间:16 带权周转时间8.0000
作业H运行时间:2 完成时间:24 周转时间:17 带权周转时间8.5000
FCFS 的平均周转时间为:11.875 带权平均周转时间:4.740
作业A运行时间:4 完成时间:4 周转时间:4 带权周转时间1.0000
作业E运行时间:3 完成时间:7 周转时间:3 带权周转时间1.0000
作业H运行时间:2 完成时间:9 周转时间:2 带权周转时间1.0000
作业G运行时间:2 完成时间:11 周转时间:5 带权周转时间2.5000
作业F运行时间:2 完成时间:13 周转时间:8 带权周转时间4.0000
作业D运行时间:3 完成时间:16 周转时间:13 带权周转时间4.3333
作业C运行时间:4 完成时间:20 周转时间:18 带权周转时间4.5000
作业B运行时间:4 完成时间:24 周转时间:23 带权周转时间5.7500
SJF 的平均周转时间为:9.500 带权平均周转时间:3.010
作业A运行时间:4 完成时间:4 周转时间:4 带权周转时间1.0000
作业B运行时间:4 完成时间:8 周转时间:7 带权周转时间1.7500
作业D运行时间:3 完成时间:11 周转时间:8 带权周转时间2.6667
作业F运行时间:2 完成时间:13 周转时间:8 带权周转时间4.0000
作业G运行时间:2 完成时间:15 周转时间:9 带权周转时间4.5000
作业H运行时间:2 完成时间:17 周转时间:10 带权周转时间5.0000
作业E运行时间:3 完成时间:20 周转时间:16 带权周转时间5.3333
作业C运行时间:4 完成时间:24 周转时间:22 带权周转时间5.5000
HRRN 的平均周转时间为:10.500 带权平均周转时间:3.719