OO第二单元作业
BearHuchao 2020年4月18日
讨论区bug交流贴1小时2回复。
给学弟学妹加需求贴15分钟20回复。
大家真是太魔鬼了,i了i了。——MountVoom
前言
本单元的主题是多线程编程。按照之前道听途说,这一单元的内容会相当鬼畜,自己在写本单元的几次作业中也碰到了很多神奇的情况。多线程的情况本就复杂,即使是在正确的情况下,每次运行的结果也有所不同。对这样的情况,调试起来更加麻烦需要更加细致地检查相关代码。
第五次作业
第五次作业是之后两次作业的基础,也是多线程的入门,需要进行的思想转变。
设计策略
基本模型
因为是第一次多线程编程,对线程安全的问题比较重视,但是知识有限,故大量使用了synconize
关键字来保证程序的线程安全性。设计的主要思路是采用生产者-消费者模型,这也是之后的作业采用的基本模式,使用RequsetQueue
类作为托盘类承接Input
类的输入,Elevator
类自带调度器。采用ALS调度算法,使用getMain
方法在电梯为空的时候从等待队列中获取主请求。同时添加RunType
类来标记电梯上下行,使用setRunType
来获取电梯运行的方向。
主要困难及解决方案
设计的主要困难是空电梯获取主请求的过程、电梯到达目标楼层的方法和电梯的运行结束。早期思路不是太明晰,空电梯获取主请求的过程在单电梯中比较容易实现,只需按照算法描述返回等待队列的队首元素即可。
电梯执行主请求的方法也比较暴力,使用一个while
循环,在电梯主请求存在的时候,使用arrive
方前往目标楼层,同时在此过程中处理捎带请求。捎带请求的处理则直接采用先验方法,即每到达一个楼层即对等待队列和电梯内乘客进行轮询得到check
的结果,如果为true
即开门处理相关请求。
电梯的结束采用的方法是主类判断Input
线程和Elevator
线程是否结束运行。同时Elevator
线程内部通过判断RequestQueue::isFinish
来判断是否继续查找主请求,否则便结束进程。
Bug分析
本次作业主要的Bug是电梯的结束问题导致的RTLE
,以及进程中的轮询操作造成的CTLE
。
相关分析
UML类图:
复杂度分析:
Method | ev(G) | iv(G) | v(G) |
---|---|---|---|
Elevator.Elevator(RequestQueue) | 1 | 1 | 1 |
Elevator.arrive(int) | 2 | 4 | 7 |
Elevator.check() | 3 | 3 | 3 |
Elevator.close() | 1 | 1 | 2 |
Elevator.getIn() | 3 | 3 | 3 |
Elevator.getMain() | 1 | 2 | 2 |
Elevator.getOff() | 1 | 3 | 3 |
Elevator.open() | 1 | 1 | 2 |
Elevator.run() | 4 | 5 | 7 |
Elevator.setRunType(int) | 1 | 1 | 3 |
Input.Input(RequestQueue) | 1 | 1 | 1 |
Input.run() | 3 | 3 | 3 |
Main.main(String[]) | 1 | 3 | 4 |
RequestQueue.RequestQueue() | 1 | 1 | 1 |
RequestQueue.add(PersonRequest) | 1 | 1 | 1 |
RequestQueue.check(int,RunType,boolean) | 3 | 6 | 6 |
RequestQueue.get(int,RunType,boolean) | 3 | 6 | 6 |
RequestQueue.getFirst() | 1 | 1 | 1 |
RequestQueue.isEmpty() | 1 | 1 | 1 |
RequestQueue.isFinished() | 1 | 1 | 1 |
RequestQueue.peekFirst() | 1 | 1 | 1 |
RequestQueue.toString() | 1 | 1 | 1 |
RunType.matches(int,int) | 4 | 2 | 4 |
Class | OCavg | WMC |
---|---|---|
Elevator | 2.8 | 28 |
Input | 2 | 4 |
Main | 2 | 2 |
RequestQueue | 1.44 | 13 |
RunType | 4 | 4 |
第六次作业
第六次作业是多个电梯并行,电梯的数量一开始给出。
另外,本次作业中使用了线程安全容器LinkedBlockingQueue
和ConcurrentHashMap
。
设计策略
基本模型
设计的主要思路是依然是采用生产者-消费者模型。采用ALS调度算法,使用getMain
方法在电梯为空的时候从等待队列中获取主请求。
主要困难及解决方案
本次作业的第一步获取电梯数目也是一个难点,需要在主线程中创建电梯,需要从Input
线程中获取电梯数目的信息。迫于无奈,使用了暴力轮询的做法来获取电梯数量的信息。
第六次作业对主请求的获取与第五次有所不同,在电梯获取主请求的时候,需要将相关请求从公共等待队列中清除,以免出现多个电梯请求同一个请求的现象。同时,在电梯真正到达目标楼层时,需要给出已经指定给该电梯的乘客请求,如果盲目给出队首元素可能会出现乘客在错误的楼层进入电梯。解决方案是在RequestQueue
类中新增一个ConcurrentHashMap
。这个成员的用处是:在电梯发出peekFirst
请求时分配乘客请求,并将其映射到对应的电梯id
上,在电梯真正到达主请求所在目标楼层后请求getFirst
,将电梯id
对应的乘客请求返回给调用电梯。
电梯的结束采取和第五次作业一致的方法。
Bug分析
本次作业主要的Bug是电梯的结束问题导致的RTLE
,以及进程中的轮询操作造成的CTLE
。
本次作业中出现的其他问题是作业需求中新增了电梯的容量限制和0楼层相关内容。
相关分析
UML类图:
代码复杂度分析:
Method | ev(G) | iv(G) | v(G) |
---|---|---|---|
Elevator.Elevator(RequestQueue,char) | 1 | 1 | 1 |
Elevator.arrive(int) | 2 | 4 | 9 |
Elevator.check() | 3 | 3 | 3 |
Elevator.close() | 1 | 1 | 2 |
Elevator.getIn() | 3 | 3 | 3 |
Elevator.getMain() | 1 | 3 | 3 |
Elevator.getOff() | 1 | 3 | 3 |
Elevator.open() | 1 | 1 | 2 |
Elevator.run() | 5 | 5 | 7 |
Elevator.setRunType(int) | 1 | 1 | 3 |
Input.Input(RequestQueue) | 1 | 1 | 1 |
Input.getNumber() | 1 | 2 | 3 |
Input.run() | 3 | 3 | 3 |
Main.main(String[]) | 1 | 6 | 7 |
RequestQueue.RequestQueue() | 1 | 1 | 1 |
RequestQueue.add(PersonRequest) | 1 | 1 | 1 |
RequestQueue.check(int,RunType,boolean) | 3 | 6 | 6 |
RequestQueue.get(int,RunType,boolean) | 3 | 6 | 6 |
RequestQueue.getFirst(char) | 1 | 1 | 1 |
RequestQueue.isEmpty() | 1 | 1 | 1 |
RequestQueue.isFinished() | 1 | 2 | 2 |
RequestQueue.peekFirst(char) | 1 | 2 | 3 |
RequestQueue.setEnd() | 1 | 1 | 1 |
RequestQueue.toString() | 1 | 1 | 1 |
RunType.matches(int,int) | 4 | 2 | 4 |
Class | OCavg | WMC |
---|---|---|
Elevator | 3.1 | 31 |
Input | 2 | 6 |
Main | 4 | 4 |
RequestQueue | 1.5 | 15 |
RunType | 4 | 4 |
第七次作业
第七次作业是最难的一次。主要的问题是电梯的换乘问题以及电梯的结束问题。
设计策略
基本模型
在前两次作业的基础上出现了换乘问题,使用静态换乘方法,没有使用复杂的换乘调度机制。
主要困难及解决方案
本次作业获取主请求的方法又更加困难。本次作业中采取的方法是将乘客分类为“可直达乘客”和“需换乘乘客”,在电梯请求peekFirst
方法时,RequestQueue
遍历等待队列,将符合条件(“可直达乘客”需要满足“乘坐当前电梯可直达目的地”;“需换乘乘客”需要满足“乘坐当前电梯可到达某一换乘楼层”)的乘客所在楼层返回,否则返回0
。对于捎带请求,运用类似的规则。
由于本次作业可能存在单个电梯无法满足请求的情况,故在进行电梯进程的结束时采用了与前两次作业不同的解决方案。主方法在Input
进程结束后,如果检测到RequestQueue
为空且所有Elevator
为空的情况下,就向RequestQueue
发送结束信号;Elevator
进程会检查RequestQueue
的状态,如果其被标记为结束,即停止执行。但是这样的设计在互测中被发现有bug,故需要进行修复。
本次作业涉及了电梯的换乘问题,个人的解决办法是:“需换乘乘客”在到达换乘楼层时,电梯会新生成一个乘客请求并将其放入RequestQueue
中,这个请求的PersonId
为原乘客PersonId
,出发楼层为当前换乘楼层,目标楼层为原目标楼层,并被标记为“可直达乘客”。
Bug分析
本次作业主要的Bug是电梯的结束问题导致的RTLE
,以及进程中的轮询操作造成的CTLE
。
本次作业出现的其他Bug是换乘相关的Bug,主要表现形式有乘客在错误的楼层进行了换乘、(与电梯进程结束条件相关)换乘乘客在换乘之后没有后续电梯执行未完成的请求。
程序结束相关
本次作业中使用的结束判定方法被证实为错误,究其原因,是线程安全容器LinkedBlockingQueue
在进行put
操作时,isEmpty
方法不会返回正确的值,导致程序提前结束,之后的换乘请求被搁置在RequestQueue
中,但是相关电梯进程已经结束,造成了互测中的bug。
相关分析
UML类图:
代码复杂度分析:
Method | ev(G) | iv(G) | v(G) |
---|---|---|---|
Elevator.Elevator(ElevatorRequest) | 1 | 1 | 1 |
Elevator.Elevator(RequestQueue,char) | 1 | 1 | 2 |
Elevator.arrive(int) | 2 | 5 | 10 |
Elevator.check() | 3 | 3 | 3 |
Elevator.close() | 1 | 1 | 2 |
Elevator.getIn() | 3 | 3 | 3 |
Elevator.getMain() | 1 | 4 | 5 |
Elevator.getOff() | 1 | 4 | 4 |
Elevator.isEmpty() | 1 | 2 | 2 |
Elevator.open() | 1 | 1 | 2 |
Elevator.run() | 5 | 5 | 7 |
Elevator.setRunType(int) | 1 | 1 | 3 |
ElevatorType.getCapacity() | 5 | 2 | 5 |
ElevatorType.getElevatorType(String) | 5 | 2 | 5 |
ElevatorType.getFloors() | 5 | 2 | 5 |
ElevatorType.getTime() | 5 | 2 | 5 |
ElevatorType.getTransferFloor(ElevatorType,RunType) | 1 | 1 | 1 |
Input.Input(RequestQueue,LinkedBlockingDeque |
1 | 1 | 1 |
Input.run() | 3 | 8 | 8 |
Main.main(String[]) | 1 | 10 | 12 |
Passenger.Passenger(PersonRequest) | 1 | 1 | 1 |
Passenger.Passenger(PersonRequest,boolean) | 1 | 1 | 1 |
Passenger.getDirectToFloor() | 1 | 1 | 1 |
Passenger.getFromFloor() | 1 | 1 | 1 |
Passenger.getPersonId() | 1 | 1 | 1 |
Passenger.getToFloor(int,ElevatorType) | 2 | 5 | 5 |
Passenger.getTransferFloor(PersonRequest) | 1 | 6 | 6 |
Passenger.isDirect() | 1 | 1 | 1 |
Passenger.matches(RunType) | 1 | 1 | 1 |
Passenger.needGetOff(int) | 1 | 3 | 3 |
Passenger.needTransfer() | 1 | 1 | 1 |
Passenger.transfer(int) | 1 | 1 | 1 |
RequestQueue.RequestQueue() | 1 | 1 | 1 |
RequestQueue.add(Passenger) | 1 | 1 | 1 |
RequestQueue.add(PersonRequest) | 1 | 1 | 1 |
RequestQueue.addBusy() | 1 | 1 | 1 |
RequestQueue.addIdle() | 1 | 1 | 1 |
RequestQueue.check(int,RunType,boolean) | 3 | 6 | 6 |
RequestQueue.get(int,RunType,ElevatorType,boolean) | 4 | 10 | 10 |
RequestQueue.getFirst(String,ElevatorType) | 1 | 1 | 1 |
RequestQueue.getIdle() | 1 | 1 | 1 |
RequestQueue.isEmpty() | 1 | 2 | 2 |
RequestQueue.isFinished() | 1 | 3 | 3 |
RequestQueue.peekFirst(String,ElevatorType,int) | 5 | 8 | 9 |
RequestQueue.setEnd() | 1 | 1 | 1 |
RequestQueue.toString() | 1 | 1 | 1 |
RunType.getRunType(PersonRequest) | 3 | 1 | 3 |
RunType.matches(int,int) | 4 | 2 | 4 |
Class | OCavg | WMC |
---|---|---|
Elevator | 3 | 36 |
ElevatorType | 4.2 | 21 |
Input | 3.5 | 7 |
Main | 7 | 7 |
Passenger | 1.67 | 20 |
RequestQueue | 1.64 | 23 |
RunType | 3.5 | 7 |
扩展性
第七次作业中出现了换乘相关的问题,由于采用的是静态换乘方案,可扩展性相对较差。可优化的部分如下文所述。
动态换乘
利用Dijkstra或Floyed算法实现电梯换乘的动态性,这样对电梯种类的扩展性更加友好。
结束条件
由于最后一次作业出现了需要电梯协作的换乘请求,这样就会导致过早地结束电梯进程可能会使得某些需要换乘的请求无法被送达至目的地。而且,提交的版本中结束条件过于复杂。在后来的bug修复过程中,本人经提示想出了一种简洁的结束判断方法,即在RequestQueue
类中新增变量记录当前未完成的请求数。当未完成的请求数为0且输入进程结束时,即可结束电梯的运行。(我实在是太傻了xxxwsm,之前那么复杂的东西怎么想出来的)
发现别人的Bug
整个互测的过程都没怎么操心……因为自己写的python脚本会出现程序不能结束的问题,故本单元互测相关的内容很少,基本上就是测试某些特殊情况(例如第三次作业中的各个楼层的换乘等)。
心得和体会
- 最后一次作业虽然强测没有问题,但是在互测环节中被查出了bug,感谢那位同学。
- 多线程的交互以及线程安全容器还需要进一步了解,防止出现这次作业中出现的请求“消失”的情况。