本次电梯有三次问题,从一台电梯到多台电梯,从傻瓜单次调度到可稍带等优化调度。难点主要在于多线程的设计,以及调度策略等。
设计思路
本次实验较好的一点是设计风格基本保持了较好的可扩展性,三次作业中没有产生明显的重构,所以这次就主要针对与第三次作做一个总结。
本次电梯我主要将人乘坐电梯这件事分为三个主体,即请求、电梯和调度器。
其中请求是最简单的一个部分,在第一和第二次作业中我直接使用了提供的PersonRequest接口,并没有给请求赋予更多的特性和功能,其控制是由电梯和调度器来进行的,其状态取决于其当前所处电梯和调度器中的数据结构的具体位置。
每个电梯都是一个线程,其从整体上来看只存在三个状态,向上,向下和暂停(即线程wait)。另外,其内部储存着其每层运行时间、开关门时间、容量、电梯名称、当前楼层、门的状态(开/关)、可到达楼层和已上电梯的请求。电梯不关心请求如何分配,不关心目标的楼层是什么(只关心是要向上还是向下),也不关心什么人需要上电梯。电梯关心的是当前楼层里有没有请求需要离开电梯,还有接下来是向下一层、向上一层还是暂停。
调度器是一个托盘,采用单例对象保证只有一个调度器且方便电梯调用其方法。调度器负责输入的处理和分解,负责处理哪些请求需要上电梯,以及接下来是什么状态。
调度方法
调度由调度器进行。其中分为请求的分析以及具体的调度
请求的分析
在调度器中,利用一个ElevatorMap来记录各个电梯可以到达得楼层的情况,使用的是类似于邻接矩阵的形式,每一层楼是一个节点,不过这里矩阵中的元素不是一个数字,而是一个储存着可达情况,以及可使用电梯和需要时间的对象。对于每个输入的请求,会在这个图上利用Dijkstra算法求出最短路径,这样就得到了我们对此请求的分解,将这个最短路径信息(包含起始楼层,到达楼层以及可以乘坐的电梯编号,以ArrayList储存)封装进继承于PersonRequest的新的请求类,并加入调度器的等待队列中。其中这个新的请求类会将第一个路径的起始和结束作为getFromFloor和getToFloor的返回值,将剩下的路径封装起来,待该请求下电梯完成当前路径后,接下来的路径就会取代之前的路径。这样就在储存请求分解方式的同时,解决了对分解后的请求顺序有要求的问题。
具体的调度
在每个电梯对象中有两个请求队列,一个队列是电梯已经接受的请求队列,另一个是电梯假装接受的请求队列。
当电梯每到一层时,先判断此层是否是自己的可停靠层,若不是则输出ARRIVE后前往下一层,若是,则首先判断有没有请求要下电梯,之后将自己传入调度器中,由调度器决定是否有人需要上电梯。首先调度器先判断当前电梯的门是否已经打开了,如果没有调度器则根据等待队列中的请求信息先判断是否有请求要在这一层出发并且方向相同且可以乘坐当前电梯,如果没有则返回。如有,则先开门,在将当前队列中所有符合出发楼层要求以及电梯要求的同方向请求存入已接受的队列中,并从调度器的等待队列中除去,对于符合出发楼层要求以及电梯要求的不同方向电梯,则放入假装接受的请求队列中,并且不从调度器的等待队列中除去。之后电梯判断门的状态,若开着则将门关闭。
这里的所谓假装接受的请求,是我为了减小算法难度,同时又想尽量多捎带一些人少开门的结果。其实等同于对这些请求做了一个标记,指示说这个请求已经进到某个电梯了,下次“不用开门就可以直接进”。我最后发现这个也完全可以给请求做标记来实现,不用给电梯单独多加一个队列来储存。当然这个操作非常的不自然,最终也的确给我带来了好多个bug,值得我的反思。
最后电梯判断当前自己已经接受符合当前方向的请求是否都已经处理完毕,若没有,则方向不变地前往下一个楼层,若已经为空则将自己传给调度器,由调度器决定下一步的方向。调度器则从等待队列中查找第一个该电梯可以接受的请求,将前往其出发楼层作为电梯的方向。若当前等待队列中不存在符合要求的请求,则让电梯线程等待(wait),直到被notify。
最终,如何判断电梯完全运行结束呢?当输入结束且三个线程都wait的时候就已经结束了,我的具体实现方法是在调度器中设置两个标志inputEnd和everythingEnd,当输入结束后就给inputEnd置True,每当电梯要wait之前,都判断一下当前everthingEnd了吗?或者inputEnd且其他两个电梯都已经wait了吗?如果是,则将everythingEnd置为True,并notify其他的线程,之后结束运行。
多线程控制
第三次作业中我作为托盘的调度器是需要做线程安全处理的对象,而其中其需要线程安全的对象就是等待队列waitingRequestList,为了使开关锁更加灵活,我使用了ReentrantLock来实现对等待队列的访问互斥,我没有将读写分来,因为调度器中的读写常常是伴随的,所以所以需要访问等待队列的操作都需要上锁,其中尤其注意在一整个对循环队列的循环中,锁不可以释放,否则会出现危险,详见BUG部分。在线程同步上,我使用了配套于Lock类的Condition类,使用其condition.await,以及condition.signalAll即相当于wait和notifyAll方法,当等待队列中没有电梯可以处理的请求时电梯就wait,当有新的请求加入时就notifyAll,其中需要注意的是不仅新的请求可以从输入中获得,需要换乘电梯的请求在中途下电梯后也会产生新的请求,同样需要notifyAll。
程序分析
下图是第一次单独电梯程序的类图,由于要求比较低,我只设置了两个类,Main和Elevator,分别作为两个线程,保证了最基本的功能。
Method | ev(G) | iv(G) | v(G) |
---|---|---|---|
Elevator.Elevator(LinkedList<PersonRequest>) | 1 | 1 | 1 |
Elevator.goToFloor(int) | 2 | 1 | 2 |
Elevator.letSomeoneIn(int) | 1 | 1 | 1 |
Elevator.letSomeoneOut(int) | 1 | 1 | 1 |
Elevator.run() | 4 | 3 | 5 |
Elevator.waitFor(int) | 1 | 1 | 2 |
Main.main(String[]) | 3 | 3 | 3 |
Class | OCavg | WMC |
---|---|---|
Elevator | 1.67 | 10 |
Main | 3 | 3 |
Package | v(G)avg | v(G)tot |
---|---|---|
2.14 | 15 |
第一次作业的程序复杂度也比较低(写的这么简易肯定低啊),只有ELevator.run由于集成度较高所以复杂度偏高一些。
在第二次作业中,我多设置了一个调度器类作为托盘使用,还是两个线程Main和Elevator,第二次作业中类之间的关系还算比较明晰,尤其是和之前求导的作业相比,没有过多的耦合。
Method | ev(G) | iv(G) | v(G) |
---|---|---|---|
Dispatcher.Dispatcher() | 1 | 1 | 1 |
Dispatcher.addRequest(PersonRequest) | 1 | 2 | 2 |
Dispatcher.anyoneGetOn(Elevator) | 4 | 13 | 13 |
Dispatcher.askDirection(Elevator) | 3 | 3 | 6 |
Dispatcher.checkDirection(PersonRequest) | 2 | 1 | 2 |
Dispatcher.getInstance() | 1 | 1 | 1 |
Dispatcher.getNewDirecion(PersonRequest,int) | 2 | 1 | 2 |
Elevator.Elevator() | 1 | 1 | 1 |
Elevator.addRequest(PersonRequest) | 1 | 1 | 1 |
Elevator.addTempInRequest(PersonRequest) | 1 | 1 | 1 |
Elevator.anyoneGetOff() | 1 | 3 | 3 |
Elevator.changeDirection(Direction) | 1 | 1 | 1 |
Elevator.changeDoorState() | 1 | 1 | 2 |
Elevator.checkRemoveTempIn() | 3 | 3 | 3 |
Elevator.closeDoor() | 1 | 2 | 2 |
Elevator.getDirection() | 1 | 1 | 1 |
Elevator.getDoorState() | 1 | 1 | 1 |
Elevator.getFloor() | 1 | 1 | 1 |
Elevator.goNextFloor() | 3 | 2 | 3 |
Elevator.goToFloor() | 1 | 1 | 4 |
Elevator.isEmpty() | 1 | 1 | 1 |
Elevator.openDoor() | 1 | 2 | 2 |
Elevator.run() | 3 | 3 | 4 |
Elevator.waitFor(int) | 1 | 2 | 3 |
Main.main(String[]) | 3 | 3 | 3 |
Class | OCavg | WMC |
---|---|---|
Dispatcher | 3.14 | 22 |
Elevator | 1.88 | 32 |
Elevator.Direction | n/a | 0 |
Elevator.DoorState | n/a | 0 |
Main | 3 | 3 |
Package | v(G)avg | v(G)tot |
---|---|---|
2.56 | 64 |
但由于我对调度器调度请求上电梯的方法的设计有一些不太合理,所以可以看到anyoneGetOn方法复杂度过大,明显高于其他方法的复杂度以及平均复杂度,最后在强测中被爆的bug也证明了这里的设计有一些问题。
第三次作业中,主要的三个类仍然是Dispatcher,Elevator和Main,剩下的三个类分别是用于拓展PersonRequest的PersonRequestNew,用于求得请求分解方法的ELevatorMap以及Map的基本元素MapCell。
Method | ev(G) | iv(G) | v(G) |
---|---|---|---|
Dispatcher.Dispatcher() | 1 | 1 | 1 |
Dispatcher.addProcessedRequset(PersonRequestNew) | 2 | 1 | 2 |
Dispatcher.addRequest(PersonRequest) | 1 | 2 | 2 |
Dispatcher.anyoneGetOn(Elevator) | 4 | 18 | 18 |
Dispatcher.askDirection(Elevator) | 3 | 5 | 7 |
Dispatcher.checkDirection(PersonRequestNew) | 2 | 1 | 2 |
Dispatcher.findFirst(int) | 3 | 2 | 3 |
Dispatcher.getInstance() | 1 | 1 | 1 |
Dispatcher.getNewDirecion(PersonRequestNew,int) | 2 | 1 | 2 |
Dispatcher.initialFloorStops() | 1 | 2 | 2 |
Dispatcher.printOutput(String) | 1 | 1 | 1 |
Dispatcher.setFloorStopsAndMap(Integer[],Integer[],Integer[]) | 1 | 4 | 4 |
Elevator.Elevator(int,int,int,Integer[]) | 1 | 1 | 1 |
Elevator.addRequest(PersonRequestNew) | 1 | 1 | 1 |
Elevator.addTempInRequest(PersonRequestNew) | 1 | 1 | 1 |
Elevator.anyoneGetOff() | 1 | 3 | 3 |
Elevator.changeDirection(Direction) | 1 | 1 | 1 |
Elevator.changeDoorState() | 1 | 1 | 2 |
Elevator.checkRemoveTempIn() | 3 | 3 | 3 |
Elevator.closeDoor() | 1 | 2 | 2 |
Elevator.getDirection() | 1 | 1 | 1 |
Elevator.getDoorState() | 1 | 1 | 1 |
Elevator.getElevatorId() | 1 | 1 | 1 |
Elevator.getFloor() | 1 | 1 | 1 |
Elevator.getIdChar() | 1 | 1 | 1 |
Elevator.goNextFloor() | 3 | 2 | 3 |
Elevator.goToFloor() | 1 | 1 | 4 |
Elevator.isEmpty() | 1 | 1 | 1 |
Elevator.isFull() | 1 | 1 | 1 |
Elevator.openDoor() | 1 | 2 | 2 |
Elevator.printOutput(String) | 1 | 1 | 1 |
Elevator.run() | 3 | 4 | 5 |
Elevator.waitFor(int) | 1 | 2 | 3 |
ElevatorMap.ElevatorMap(Integer[],Integer[],Integer[]) | 1 | 9 | 9 |
ElevatorMap.dis(int,int) | 2 | 2 | 2 |
ElevatorMap.dr(int) | 1 | 1 | 1 |
ElevatorMap.r(int) | 1 | 1 | 1 |
ElevatorMap.shortestWay(int,int) | 4 | 6 | 10 |
Main.main(String[]) | 3 | 3 | 3 |
MapCell.MapCell(int,int,int) | 1 | 1 | 1 |
MapCell.checkAndModify(int,int) | 1 | 1 | 2 |
MapCell.getDis() | 1 | 1 | 1 |
MapCell.getElevators() | 1 | 1 | 1 |
MapCell.getEnd() | 1 | 1 | 1 |
MapCell.getStart() | 1 | 1 | 1 |
OutputProcess.main() | 1 | 1 | 1 |
PersonRequestNew.PersonRequestNew(ArrayList<MapCell>,PersonRequest) | 1 | 1 | 2 |
PersonRequestNew.getNextRequest() | 1 | 1 | 1 |
PersonRequestNew.isIntheList(Integer) | 1 | 1 | 1 |
PersonRequestNew.resetList(Integer) | 1 | 1 | 1 |
Class | OCavg | WMC |
---|---|---|
Dispatcher | 2.83 | 34 |
Elevator | 1.76 | 37 |
Elevator.Direction | n/a | 0 |
Elevator.DoorState | n/a | 0 |
ElevatorMap | 4.4 | 22 |
Main | 3 | 3 |
MapCell | 1.17 | 7 |
OutputProcess | 1 | 1 |
PersonRequestNew | 1.25 | 5 |
Package | v(G)avg | v(G)tot |
---|---|---|
2.46 | 123 |
第三次作业中复杂度最高的仍然是anyoneGetOn方法,但万幸的是这次bug不是出现在这里...ELevatorMap中的求最短路的方法复杂度也较高,但实际上此方法的思想并不复杂,没有太多的细节。
对于SOLID原则,我认为我这次并没有很好地贯彻,在编码的时候对其认识还不够,只有单一责任原则做的稍微较好,开放封闭原则有一些的关注,但我这次程序设计中并没有使用继承或者工厂模式,所以在里氏替换和依赖倒置原则上也较难评判,在接口隔离上我觉得我做的不够好,在设计中我也注意到相关问题但在已经形成的框架下没有找到好的解决方法,在以后的设计中我会从更加整体的角度来考虑设计。
BUG
第一次作业题目比较简单,所以在强测和互测中均没有出现问题,但在线下自己的调试中的确发现了很多之前没有注意到的事情,大多是关于多线程的互斥等方面。
第二次作业中随着复杂度的上升,对线程控制的难度也有所上升。不过这次的bug虽然与线程控制有关,但却是因为我对迭代器认识不足造成的。之所以使用迭代器来遍历List是因为迭代器可以实现在循环中的remove,我想当然地就认为使用迭代器时任何对队列的操作都不会产生什么影响,但事实上由于我在迭代中可能会由于需要开门而进入睡眠,导致睡眠中如果有新的请求插入队列,再返回后迭代器就会报出ConcurrentModificationException。导致我的强测炸掉了一半的点,但为什么线下没有发现呢?因为我没有来得及写定时输入,所有的测试都是手动输入,所以就不太会出现这种问题,但是在定时输入下这种问题出现的几率就非常非常大了,仔细想想炸掉一半的点其实已经算很少了。解决方法就是在这之前专门寻找一次有没有需要开门的请求,如果需要开门并睡眠,则在返回后直接break,避免调用迭代器,就不会有这种问题了,之后再在另外一个不会sleep的循环里正常使用迭代器就可以了。
有了第二次惨痛的教训,在第三次的作业中我当然就更加小心,使用了自动化测试对电梯进行测试,但还是在测试的时候遗漏掉了一个点而被强测找了出来,这个bug是由于我有所谓假装接受请求的策略,然而我在查看当前电梯是否为空时没有考虑假装接受的请求的队列,再加上我在调度请求的一些其他操作,导致了我可能会在电梯满员的时候还假装接受其他的请求。但是虽然我是假装接受,但实际上白纸黑字的输出IN,评测机可不管你假装不假装啊唉!
在查别人的bug时我主要使用自动化评测,查到了三个人的bug但在评测系统上只刀中了一个bug,剩下两个人的bug难以复现,但的确实实在在存在,这就提醒我们即使我们顺利通过了强侧和互测,也不代表我们的代码是百分之百正确的。除了自动化评测,在第三次互测中我发现了有一位同学有和我第二次互测中相同的问题,不过他的问题比较隐蔽难以发现,需要非常巧合的数据才可以造成bug,我构造了很久的数据也难以实现。、
心得体会
这三次电梯作业和上一单元的求导作业相比少了输入处理的难度,给设计减少了很多负担,增添了不少乐趣。本次作业主要关注于多线程的设计,多线程程序的设计与以往的程序有很大的不同,需要考虑多个线程之间并行运行,需要考虑他们之间的资源互斥与信息传递,而多线程调度的不确定性也给设计带来了很多困难,这些需要去学习去适应的。在编码调试的过程中也发现,以往的调试方法可能已经失效了(有时候还是可以用的),并且多线程的不确定性使得同样的输入数据有时候可以正确运行,有时候却不能,我们需要做到的不仅是让程序可以在一次运行中得到正确的结果,而是使得程序在每次运行中都得到正确解,这就需要我们对多线程程序的设计有比较深的体会对多线程中容易出现的问题有足够的敏锐性,也要遵循多线程程序设计中的准则来避免问题的发生。