据说,每个程序员都会在等电梯的时候,思考电梯调度算法;又据说,每个6系学子在写完自己的电梯后都无比庆幸,还好没有搭乘过自己写的电梯2333……
难以复现的多线程bug,没有边界的电梯调度策略优化,这一切看似玄之又玄的问题,恰恰是吸引着我们不断去探索的众妙之门……
下面,我就自己的这一单元电梯作业做一下总结。
第五次作业——傻瓜电梯(FAFS)
设计策略
程序采用简单的生产者消费者模型。
生产者:乘客,产生请求
消费者:电梯,处理请求
中间设置一个调度器,盛放已经输入还未被处理的请求。调度器的数据结构为LinkedBlockingQueue。
程序共有三个线程:主线程,输入线程和电梯线程。
程序分析
类图
时序图
复杂度分析
Method | ev(G) | iv(G) | v(G) |
---|---|---|---|
Elevator.Elevator() | 1 | 1 | 1 |
Elevator.close() | 1 | 1 | 1 |
Elevator.getTime(int,int) | 1 | 1 | 2 |
Elevator.open() | 1 | 1 | 1 |
Elevator.personIn() | 1 | 1 | 1 |
Elevator.personOut() | 1 | 1 | 1 |
Elevator.run() | 4 | 6 | 8 |
Input.Input() | 1 | 1 | 1 |
Input.run() | 3 | 4 | 4 |
TaskQueue.TaskQueue() | 1 | 1 | 1 |
TaskQueue.addRequest(PersonRequest) | 1 | 1 | 1 |
TaskQueue.getRequest() | 1 | 1 | 2 |
TaskQueue.isEmpty() | 1 | 1 | 1 |
TaskQueue.isEnd() | 1 | 1 | 1 |
TaskQueue.setEnd(boolean) | 1 | 1 | 1 |
Test.main(String[]) | 1 | 1 | 1 |
由图可见,Elevator.run()方法基本复杂性较高,这样的话,程序较难理解与维护。仔细分析之,确实如此,run()方法中,我会修改一些全局变量,Thread.sleep()也在这里完成,目前想到的比较好的修改方案是,不设置那么多全局变量,直接传递参数,Thread.sleep()在开关门上下行方法内部实现。
优缺点
优点:结构较为清晰,按照生产着消费者模式组织代码;调度器采用LinkedBlockingQueue容器,它是线程安全的,实现了先入先出等特性。
缺点:在没有请求时,采用了sleep等待轮询的方式,浪费了CPU时间;Elevator.run()方法复杂度较高。
SOLID原则
单一责任原则:输入类,调度器类,电梯类协同配合,各司其职。稍有不足是电梯类run方法修改全局变量,稍有冗余。
开放封闭原则:第六次作业完全在第五次作业基础上进行优化,扩展性良好。
里氏替换原则:仅有线程类用到了继承。
接口分离原则:没有使用接口。
依赖倒置原则:高层次模块没有依赖低层次模块。
我的bug分析
未发现bug
发现别人bug策略
未发现bug
第六次作业——实现捎带的单部电梯(SCAN)
设计策略
本电梯沿用第一次作业电梯架构。
实现捎带策略:
在进行多方权衡(ALS考虑因素较多,性能不优;SCAN算法虽需要多次遍历请求队列,但遍历请求队列对CPU时间影响较小),本次电梯采用SCAN算法来实现捎带。
考虑到,每一趟上下行,电梯要尽可能地多带人,我想实现先捎取第一个人来确定电梯运行的方向,再沿路捎带的算法。为了尽可能地优化性能,我选取第一个人的方式如下:如果上行的人更多,我就选取等候上行的请求中楼层最低的人作为首个请求;反之,选择等候下行请求中楼层数最高的人作为首个请求;如果电梯的当前楼层就有人等候,我把这个人当做第一个请求。
那如果电梯在开门的时间内,来了请求,怎么办呢?我是这样处理的:在开门后,再遍历一遍请求队列,查询有没有符合捎带情况的请求。
程序分析
类图
时序图
复杂度分析
Method | ev(G) | iv(G) | v(G) |
---|---|---|---|
Elevator.Elevator() | 1 | 1 | 1 |
Elevator.addTaker(PersonRequest) | 1 | 6 | 6 |
Elevator.arrive() | 1 | 1 | 1 |
Elevator.close() | 1 | 1 | 1 |
Elevator.copeInOut(PersonRequest) | 1 | 9 | 9 |
Elevator.getTime(int,int) | 1 | 1 | 2 |
Elevator.gotoDestination(int) | 3 | 3 | 5 |
Elevator.isValid(int,int,int) | 3 | 1 | 5 |
Elevator.listInto(LinkedList<PersonRequest>) | 1 | 3 | 3 |
Elevator.open() | 1 | 1 | 1 |
Elevator.personIn(Integer) | 1 | 1 | 1 |
Elevator.personOut(Integer) | 1 | 1 | 1 |
Elevator.run() | 8 | 9 | 12 |
Elevator.setStatus(PersonRequest) | 1 | 1 | 2 |
Input.Input() | 1 | 1 | 1 |
Input.run() | 3 | 4 | 4 |
TaskQueue.TaskQueue() | 1 | 1 | 1 |
TaskQueue.addRequest(PersonRequest) | 1 | 2 | 2 |
TaskQueue.getFirstRequest(int) | 4 | 8 | 11 |
TaskQueue.getInstance() | 1 | 1 | 2 |
TaskQueue.getRequest(Integer,Integer) | 1 | 4 | 4 |
TaskQueue.isEmpty() | 1 | 1 | 1 |
TaskQueue.isEnd() | 1 | 1 | 1 |
TaskQueue.setEnd(boolean) | 1 | 1 | 1 |
Test.main(String[]) | 1 | 1 | 1 |
由图可见,Elevator.run()方法的三个复杂性均较高,Elevator.copeInOut()的设计复杂度较高,Elevator.getFirstRequest()方法基本复杂性较高。出现这样问题的原因是,基本的调度算法与优化策略基本上全体现在getFirstRequest()和run()方法中,这样的话,优化措施越多,优化逻辑越复杂,程序的基本复杂度就会大大提升。因为run()方法要分多种模式实现,有点类似与一个状态机,如果处理地不太好,各个状态界限不分明,各个方法的耦合性就会提升,进而设计复杂度也会提高。
优缺点
优点:结构较为清晰,按照生产着消费者模式组织代码;采用的电梯调度策略是对SCAN算法的一种优化,可以在性能上达到更优。
缺点:在设计时,有些较为复杂的模块复杂性还存在较高的情况,应该更合理地去设计分析。
SOLID原则
单一责任原则:输入类,调度器类,电梯类协同配合,各司其职。稍有不足是电梯类没有很好地进行状态的判断与分离,使得有些类的耦合性稍高。
开放封闭原则:第七次作业在第六次作业的基础上修改,扩展性良好。
里氏替换原则:仅有线程类用到了继承。
接口分离原则:没有使用接口。
依赖倒置原则:高层次模块没有依赖低层次模块。
我的bug分析
未发现bug
发现别人bug策略
未发现bug
第七次作业——实现捎带的三部电梯(SCAN)
设计策略
本电梯沿用第六次作业电梯架构。架构上进行的修改是,我把上一次电梯的调度器存进电梯中,作为每部电梯的子调度器;另外设置一个总调度器,实现指令的拆分与分配工作。
对于多电梯的情况,我采用比较简单的拆分指令策略。考虑到简单地拆分指令,把它分配到各个电梯的子调度器中。这样做就会面临一个问题:一个指令如果有多种拆分方式,我们要怎么拆分他们呢?
因为不同的拆分方式可能会造成性能上的很大不同,我经过权衡比较,采取如下方式优化:
1、将指令尽可能地分配到一个电梯中,这样的话,会尽可能地减少换乘所带来的时间损失
2、将必须通过拆分的指令,尽可能地在1、15层投放出去。这样的话,会尽可能地集中换乘的人流在同一楼层上下楼,减少开关门时间损失。
3、在单电梯可实现的捎带策略中,C电梯的优先级更高。在进行大量的数据测试后,我发现,自己的程序对C电梯的利用率极低。若非必须要到3楼,我几乎没有用到过C电梯。这样的话,就白白地闲置了一个电梯资源。研究我发现,大多C电梯能够实现的捎带策略,B电梯也能够实现;而B电梯的很多请求,C电梯却无法处理。如果使用B电梯优先级更高,就会造成B电梯过劳,C电梯过于摸鱼。(即使能者多劳也不能这么欺负人家吧)于是,我把C电梯的优先级提高。
那么,对于拆分过的指令,有一个时序上的条件我们必须满足:经拆分的后半条指令必须在前半条执行完毕后,才能执行。我是在子调度器中,新增了被blocked的PersonRequest容器HashMap,索引是每个请求独一无二的id编号。当上半条指令执行完毕后,便会搜寻另外两个电梯中是否有被blocked同一id号,如果有的话,将其从blocked中移除,转到等待被调度的队列中。
程序分析
类图
时序图
由于三部电梯的调度策略基本上完全相同,在图中,只显示A电梯。
复杂度分析
Method | ev(G) | iv(G) | v(G) |
---|---|---|---|
Elevator.Elevator(int,long,String) | 1 | 1 | 1 |
Elevator.addRequest(PersonRequest) | 1 | 1 | 1 |
Elevator.addTaker(PersonRequest) | 1 | 6 | 6 |
Elevator.addWaitingRequest(PersonRequest) | 1 | 1 | 1 |
Elevator.arrive() | 1 | 1 | 1 |
Elevator.close() | 1 | 1 | 1 |
Elevator.copeInOut(PersonRequest) | 1 | 9 | 9 |
Elevator.getTime(int,int) | 1 | 1 | 2 |
Elevator.gotoDestination(int) | 3 | 3 | 5 |
Elevator.isValid(int,int,int) | 3 | 1 | 5 |
Elevator.listInto(LinkedList<PersonRequest>) | 1 | 3 | 3 |
Elevator.open() | 1 | 1 | 1 |
Elevator.personIn(Integer) | 1 | 1 | 1 |
Elevator.personOut(Integer) | 1 | 3 | 3 |
Elevator.run() | 8 | 11 | 14 |
Elevator.setBounded(Elevator,Elevator) | 1 | 1 | 1 |
Elevator.setEnd() | 1 | 1 | 1 |
Elevator.setStatus(PersonRequest) | 1 | 1 | 2 |
Input.Input() | 1 | 1 | 1 |
Input.run() | 3 | 4 | 4 |
Scheduler.Scheduler() | 1 | 1 | 1 |
Scheduler.addRequest(PersonRequest) | 2 | 3 | 12 |
Scheduler.analyseRequests(PersonRequest) | 10 | 10 | 19 |
Scheduler.getInstance() | 1 | 1 | 2 |
Scheduler.setElevatorA(Elevator) | 1 | 1 | 1 |
Scheduler.setElevatorB(Elevator) | 1 | 1 | 1 |
Scheduler.setElevatorC(Elevator) | 1 | 1 | 1 |
Scheduler.setEnd(boolean) | 1 | 1 | 1 |
Scheduler.setReachA() | 1 | 3 | 3 |
Scheduler.setReachB() | 3 | 3 | 4 |
Scheduler.setReachC() | 1 | 2 | 2 |
TaskQueue.TaskQueue() | 1 | 1 | 1 |
TaskQueue.addRequest(PersonRequest) | 1 | 2 | 2 |
TaskQueue.addWaitingRequest(PersonRequest) | 1 | 1 | 1 |
TaskQueue.getBlocked() | 1 | 1 | 1 |
TaskQueue.getFirstRequest(int) | 4 | 8 | 11 |
TaskQueue.getRequest(Integer,Integer,Integer) | 4 | 4 | 6 |
TaskQueue.isEmpty() | 1 | 1 | 1 |
TaskQueue.isEnd() | 1 | 1 | 1 |
TaskQueue.remove(int) | 1 | 1 | 1 |
TaskQueue.setEnd(boolean) | 1 | 1 | 1 |
Test.main(String[]) | 1 | 1 | 1 |
由复杂度分析表格可知,这次作业程序复杂度较高的地方与上次作业有相同之处。新增的全局调度器也有一些方法复杂性较高。Scheduler.addRequest()总体复杂性较高;Scheduler.analyseRequests()三个复杂性均较高。但是对于这些方面,我会存在一些疑问,我认为这些地方因为调度、优化的需要,无可避免地会造成逻辑上的复杂,进而引起复杂性提高。如果既想要优化,有想要程序美观、易于理解、耦合性低。这又该怎么实现呢?
优缺点
优点:结构较为清晰,按照生产着消费者模式,和两级调度器,能够较好地分配指令。
缺点:优化的极限是取决于架构的。当前的架构还没有办法实现更好的优化,比如说,看电梯运行情况分配请求。
SOLID原则
单一责任原则:基本上所有的类能够各司其职。
开放封闭原则:架构易于扩展。但是如果想要优化到更高层次,必须跳脱出架构的束缚。
里氏替换原则:仅有线程类用到了继承。
接口分离原则:没有使用接口。
依赖倒置原则:高层次模块没有依赖低层次模块。
我的bug分析
在强测阶段由于电梯可能会超载一人,被爆了5个点。bug出现的原因是,自己的这块代码写的还是不够清晰,对电梯当前人数的修改,也没有能够保证线程安全。
发现别人bug策略
采用自动化测试,发现别人的bug。
一点点心得
在这三次作业中,因为多线程安全问题犯过错误,但这些错误也教会了我去更加缜密地去测试程序。有时候,我们看似多次测试,没有出现的问题说不定就在强测中暴露;这时,就需要我们在设计时就对线程安全可能产生的问题进行提前思考与事先规避。只有设计地更加合理,写代码时不漏考虑任何一种可能引起线程安全问题的细节,才能够有效规避强测中的“爆炸”。
同时,第三次作业时,我的架构决定着我的优化极限。我只能在拆分指令的基础上尽可能地优化;而不能打破边界,实现人通过对电梯运行条件的判断,“智能”地进入电梯。因此,在设计的时候,我们要考虑优化的合理性,程序的扩展性。而不能只抱有先完成基本功能、再考虑优化的思想。因为,可能在优化的时候,我们会尴尬地发现,自己的程序似乎已经没有办法在优化啦?