1.作业设计策略
1.1第一次作业
第一次作业指导书要求是一个单部多线程傻瓜调度(FAFS)电梯的模拟,由于为了可扩展性和模块化设计,第一次作业我采用了三线程,即输入处理线程,调度器线程,电梯线程这三个线程进行通信工作,当然,实际上调度器线程仅仅做了把输入的请求进行转交给电梯线程这一个工作。输入处理线程和调度器线程共享reqlist
这个公共类进行消息传递,通过两个互斥的操作add
和fetch
进行队列任务的增加和提取,然后由调度器线程把提取到的任务交给电梯线程的worklist
,电梯拿到任务遍将任务从worklist
放到runlist
中,然后电梯进行模拟运行。我第一次作业的设计中有一个很糟糕的问题,就是调度器采取了暴力轮询的方式。
1.2第二次作业
第二次作业指导书要求是单部多线程可捎带调度(ALS)电梯的模拟,由于这次的任务其实我在第一次作业考虑到了,所以线程还是三线程,输入处理线程,调度器线程,电梯线程,也就是说我的基本架构没有变化,但是在调度器和输入处理线程的通信增加了wait
和notifyall
的线程维护操作,保证了没有暴力轮询的问题,当然这两个线程的通信还是依赖于reqlist
这个公共类。
这次作业在设计的时候我思考了一个架构问题,那就是我是让调度器进行捎带任务的处理还是让电梯本身进行任务的重新排序,在一番纠结后,我采取了伪智能化电梯的构建,因为我一开始的电梯框架包括了两个任务队列,一个是待运行队列worklist
,另一个是电梯当前运行负载队列runlist
,所以我可以依靠这两个队列进行任务更新工作,而且我的电梯是一个状态机的模拟,所以我可以在状态转换间进行任务分析处理。所以,我的调度器线程在这次作业依旧只是起到了一个消息传递员的工作。
1.3第三次作业
第三次作业指导书要求是多部多线程智能(SS)调度电梯的模拟,这次作业由三个不同类型的电梯组合,这三个电梯的运行时间,载客数量,可达楼层都不一样,这次的作业大大超出了我之前架构可扩展性的预期,导致了我在进行架构设计的大改。由于前两次作业保证了我电梯基础架构的正确性,所以基本结构并没有变化,线程还是三线程,调度器和输入处理线程通过reqlist
进行通信。由于这次的任务分配需要站到一个高点进行统筹规划,所以我将调度器线程作为这个高点,调度器可以拿到三个电梯线程的状态信息,由调度器线程对任务和电梯状态进行分析,然后由调度器线程把任务交付给各个电梯,电梯进行模拟运行。
由于这次作业的停止消息和拆分任务消息存在问题,因为我的停止线程操作是由输入处理线程发送一个从0层到0层的任务给调度器,然后输入处理线程停止,调度器接收之后把这个消息传递给所有电梯,然后调度器停止,电梯在接收到停止信息后,在处理完任务队列后由状态机停止线程工作。但是由于电梯的任务中可能包含换乘任务,第三次作业我的req
类中曾加了一个secondreq
信息,也就是换成信息,在当前任务完成后将换乘任务传给调度器,那么如果停止信息来了,有可能调度器先于换乘电梯任务完成之前先停了,所以我依靠与操作系统课的信号量锁mutex
解决了这个问题,这个mutex
限制了当前的任务完成状态,具体在bug
部分着重说明。
其实,我这次作业为了保证正确性第一原则,所以我并没有进行任务的特殊切割,所以其实我的调度器线程也只是一个拿到任务,然后进行一个粗略估算,把任务进行分配或者切割分配,电梯线程在拿到调度器线程给的任务后,再内部进行任务二次分析,电梯进行任务运行。
2基于度量分析程序结构
首先,由于我的程序架构是一个递增的过程,所以大体上层次不会有特别多变化,所以重点分析第三次作业的结构
2.1第一次作业和第二次作业
由于我第一次作业的设计考虑到了第二次作业,所以基本结构的方法都没有变化,这里统一分析。
UML类图
UML时序图
代码度量
method | ev(G) | iv(G) | v(G) |
---|---|---|---|
Elevator.Elevator(int) | 1.0 | 1.0 | 1.0 |
Elevator.excrunlist(int,int) | 1.0 | 1.0 | 1.0 |
Elevator.fetch(Req) | 1.0 | 4.0 | 4.0 |
Elevator.getCurf() | 1.0 | 1.0 | 1.0 |
Elevator.getDirec() | 1.0 | 1.0 | 1.0 |
Elevator.getRtof() | 1.0 | 1.0 | 1.0 |
Elevator.getStatus() | 1.0 | 1.0 | 1.0 |
Elevator.initmap() | 1.0 | 3.0 | 3.0 |
Elevator.isStar() | 1.0 | 1.0 | 1.0 |
Elevator.run() | 5.0 | 10.0 | 14.0 |
Elevator.runtime(long) | 1.0 | 2.0 | 2.0 |
Elevator.seekupdate() | 3.0 | 3.0 | 4.0 |
Elevator.seerun() | 1.0 | 2.0 | 3.0 |
Elevator.updatelist() | 1.0 | 9.0 | 9.0 |
Main.main(String[]) | 3.0 | 4.0 | 4.0 |
Req.getDirection() | 3.0 | 1.0 | 3.0 |
Req.getFromf() | 1.0 | 1.0 | 1.0 |
Req.getPid() | 1.0 | 1.0 | 1.0 |
Req.getTof() | 1.0 | 1.0 | 1.0 |
Req.initmap() | 1.0 | 3.0 | 3.0 |
Req.Req(int,int,int) | 1.0 | 1.0 | 1.0 |
Req.toString() | 1.0 | 1.0 | 1.0 |
Reqlist.add(Req) | 1.0 | 1.0 | 1.0 |
Reqlist.fetch() | 2.0 | 3.0 | 4.0 |
Reqlist.get(int) | 2.0 | 2.0 | 2.0 |
Reqlist.remove(int) | 1.0 | 1.0 | 1.0 |
Reqlist.Reqlist() | 1.0 | 1.0 | 1.0 |
Reqlist.size() | 1.0 | 1.0 | 1.0 |
Scheduler.run() | 1.0 | 3.0 | 4.0 |
Scheduler.Scheduler(ArrayList,Reqlist) | 1.0 | 1.0 | 1.0 |
Total | 42.0 | 66.0 | 76.0 |
Average | 1.4 | 2.2 | 2.533333333333333 |
class | ocavg | wmc |
---|---|---|
Elevator | 3.0714285714285716 | 43.0 |
Main | 4.0 | 4.0 |
Req | 1.5714285714285714 | 11.0 |
Reqlist | 1.5 | 9.0 |
Scheduler | 2.5 | 5.0 |
Total | 81.0 | |
Average | 2.6129032258064515 | 11.571428571428571 |
前两次作业复杂度较高的主要是电梯的run
方法和更新队列的updatelist
方法,第二个方法是电梯的中心方法,这个方法包括了下电梯的判断,上电梯的判断,电梯的最优主任务的判断,导致了这个方法十分长且功能繁多。Main和Elevator两个分别是主函数且包含了输入处理方法,另一个是电梯的类,这两个类复杂度较高,是因为我的电梯需要自身去判断任务的更新和排列,主函数需要创建各个线程。
2.2第三次作业
UML类图
UML时序图
代码度量
method | ev(G) | iv(G) | v(G) |
---|---|---|---|
Elevator.addf() | 1.0 | 1.0 | 2.0 |
Elevator.cancarry(Req) | 7.0 | 4.0 | 8.0 |
Elevator.Elevator(String,int,int,Reqlist) | 1.0 | 1.0 | 1.0 |
Elevator.excrunlist(int,int) | 1.0 | 1.0 | 1.0 |
Elevator.fetch(Req) | 1.0 | 4.0 | 4.0 |
Elevator.full() | 1.0 | 1.0 | 1.0 |
Elevator.getCurf() | 1.0 | 1.0 | 1.0 |
Elevator.getDirec() | 1.0 | 1.0 | 1.0 |
Elevator.getRtof() | 1.0 | 1.0 | 1.0 |
Elevator.getStatus() | 1.0 | 1.0 | 1.0 |
Elevator.isStar() | 1.0 | 1.0 | 1.0 |
Elevator.peo() | 1.0 | 1.0 | 1.0 |
Elevator.run() | 5.0 | 11.0 | 15.0 |
Elevator.runtime(long) | 1.0 | 2.0 | 2.0 |
Elevator.seekupdate() | 4.0 | 3.0 | 5.0 |
Elevator.seerun() | 1.0 | 2.0 | 3.0 |
Elevator.subf() | 1.0 | 1.0 | 2.0 |
Elevator.updatelist() | 6.0 | 14.0 | 15.0 |
Elevator.zhurenwu() | 5.0 | 7.0 | 7.0 |
Main.main(String[]) | 3.0 | 3.0 | 3.0 |
Req.getDirection() | 3.0 | 1.0 | 3.0 |
Req.getFromf() | 1.0 | 1.0 | 1.0 |
Req.getPid() | 1.0 | 1.0 | 1.0 |
Req.getsecond(Req) | 1.0 | 1.0 | 1.0 |
Req.getTof() | 1.0 | 1.0 | 1.0 |
Req.havasecond() | 1.0 | 1.0 | 1.0 |
Req.Req(int,int,int) | 1.0 | 1.0 | 1.0 |
Req.toString() | 1.0 | 1.0 | 1.0 |
Reqlist.add(Req) | 1.0 | 1.0 | 1.0 |
Reqlist.addmutex() | 1.0 | 1.0 | 1.0 |
Reqlist.addnull(Req) | 1.0 | 2.0 | 3.0 |
Reqlist.fetch() | 2.0 | 3.0 | 4.0 |
Reqlist.Reqlist() | 1.0 | 1.0 | 1.0 |
Reqlist.submutex() | 1.0 | 1.0 | 1.0 |
Scheduler.can(ArrayList,Req) | 3.0 | 2.0 | 3.0 |
Scheduler.choose(int,int) | 2.0 | 1.0 | 2.0 |
Scheduler.choose(int,int,int) | 4.0 | 2.0 | 4.0 |
Scheduler.cn1(Req,int) | 1.0 | 1.0 | 1.0 |
Scheduler.deliver(Req) | 1.0 | 34.0 | 34.0 |
Scheduler.init() | 1.0 | 4.0 | 4.0 |
Scheduler.run() | 1.0 | 5.0 | 5.0 |
Scheduler.Scheduler(ArrayList,Reqlist) | 1.0 | 1.0 | 1.0 |
Scheduler.split(Req) | 10.0 | 15.0 | 15.0 |
Scheduler.split2(Req) | 1.0 | 7.0 | 7.0 |
Total | 86.0 | 150.0 | 172.0 |
Average | 1.9545454545454546 | 3.409090909090909 | 3.909090909090909 |
class | ocavg | wmc |
---|---|---|
Elevator | 3.4210526315789473 | 65.0 |
Main | 3.0 | 3.0 |
Req | 1.25 | 10.0 |
Reqlist | 1.5 | 9.0 |
Scheduler | 5.6 | 56.0 |
Total | 143.0 | |
Average | 3.25 | 20.428571428571427 |
第三次作业设计我的调度器进行了大幅度的更新,导致scheduler复杂度过高,电梯由于设计也是自己进行任务的分析调度,写的时候没有很好的控制方法的细节,导致Elevator类也不是很好,这些设计细节我下个单元应该进行模块细化和整体性构建,避免过大的方法和类的出现。可以看出这次的设计与前两次有一个不同,就是Scheduler类的复杂度提高了,这是因为我的调度器需要实现高瞻远瞩的方法,需要对任务和电梯进行统筹规划,所以调度器的方法写了很多,包括拆分任务和找到最优分配的电梯,导致了复杂度有一个大的提升。
2.3SOILD原则分析
oo5 | oo6 | oo7 | |
---|---|---|---|
SRP-单一功能 | 电梯,调度器,输入处理线程各自完成各自任务 | 同oo5 | 同oo5 |
OCP-开闭原则 | 基础状态机搭建 | 没有对于run方法改动,只是对于调度刷新队列的方法进行重构 | 调度器进行分配任务方法在原有基础上的重构,电梯进行功能重构 |
LSP-里式替换 | 作业设计不涉及父子类问题 | 同oo5 | 同oo5 |
ISP-接口隔离 | 作业设计不涉及接口问题 | 同oo5 | 同oo5 |
DIP-依赖反转 | 不存在对类的抽象,所有都实例化 | 同oo5 | 同oo5 |
3分析程序bug
由于我没有互测环节,所以只进行论述强测和自己的测试。
3.1第一次作业
第一次作业由于是傻瓜调度,所以强测无错误出现,但是在调度器进行任务分配的时候采取了暴力轮询的方式,这点在第二次作业变成了错误点,需要改正,也反映了我一开始对于线程知识的欠缺。
3.2第二次作业
第二次作业我再进行测试的时候发现如果上限30十个任务同时输入的时候,由于是阻塞读入还有多线程的传递机制,导致可能我的电梯不认为是同一时刻的请求,在10个任务左右的接收就会开始运行,所以后续任务无法进行统筹安排,如果后几次任务为从1楼开始,那么电梯不会捎带,经与同学讨论,最终在电梯每次开始运行前进行线程sleep
10ms的时间,让可能的任务全部分配给电梯再开始工作(强测中有这样的点,同一时间全部输入,在后面的输入有1楼开始的任务)。强测虽然全部正确,但是强测我的分数不尽理想,因为我的调度挖了一个坑,我的捎带考虑的是只要在主任务的覆盖范围内,无论上行下行我都会把这个任务接上,但是由于设计的漏洞,导致可能最开始的任务使得整个队列进行一次上下的复杂运行。
3.3第三次作业
第三次作业我在整体写完后,发现测试点不能通过,思考后,发现如果有一个待拆分的任务前半段正在运行的时候,输入请求通知结束,那么我的输入处理线程会给调度器传递结束信息,这个时候调度器本身不存在任务,他会给电梯发送停止信息,但是它本身会直接线程停止,而这样电梯就无法把后半段任务传回调度器,导致了提前截至的问题,这个问题我通过了之前操作系统课学习的信号量锁解决了,也就是接到一个任务mutex
加一,差分一个任务mutex
加一,完成一个任务mutex
减一,只有当mutex
到0的时候输入线程才会把结束信息传给调度器,解决了这次作业的一个结构bug。强测最终没有出现错误,但是由于没有做很好的优化,最后得分也只有90多一点。
4心得体会
相较于第一单元的多项式作业,电梯的出现考验了我们对于多线程的理解,尤其是锁的概念尤为重要,无论是哪次电梯作业都靠考虑互斥操作的关系,怎么能尽量增加线程的独立性。另外就是电梯需要对于线程进行wait
和notifyall
操作,保证cpu时间,这就要去观察各个线程之间的联系,什么时候饥饿,什么时候满。当然,有了第一单元次次重写的教训,这一次的作业我在第一次就预留了大量的可添加模块,基本实现了模块化设计,所以我的第二次作业基本没有花费太多时间,虽然第三次超出了我的框架设计,但是经过基本变换之后整体的设计也没有大的变化,所以正确性有了很大的保证,但是恰恰就是第一次框架的设计,导致了后面我的优化无法进行很好的操作,受限于框架的固化,只能做一些基础的优化,这点也是我在下个单元需要着重注意的问题,怎么才能一开始设计出扩展性好的和优化可实行的框架,确实值得推敲。