2020北航OO第二单元总结
前言
本单元考察基于多线程的电梯调度问题,成功让我从一个多线程小白到了基本掌握了使用锁来控制线程安全的能力,收获颇多(充分体验了迷茫地de一个又一个死锁bug的痛苦)。
三次作业的关键如下:
第一次作业:单台电梯的调度,电梯可到达所有楼层,容量不设限,考虑捎带。
第二次作业:多台电梯的调度,通过输入控制电梯台数,电梯可到达所有楼层,容量受限,考虑捎带。
第三次作业:3+n台电梯的调度,通过输入随时增加电梯,电梯到达楼层、容量、运行时间分类受限,考虑换乘和捎带。
一、设计策略分析
本单元的电梯设计,我主要是利用了生产者消费者模式,将电梯需求作为生产者,电梯运载乘客作为消费者,将调度器作为托盘,处理需求并反馈当前需求信息,并将调度器设为单例模式,作为共享对象,供所有类访问。
对于请求队列的设计,我将总的请求队列存在调度器中,分为上行队列和下行队列,并为电梯设计自己的内部队列,存储已经进入电梯的需求。电梯在输入结束,并且所有队列均为空时,结束电梯线程。
总的工作模式,是采用输入将需求加入调度器中的请求队列,电梯根据调度器所返回的请求信息,主动向调度器申请请求的方式。
第一次作业:我采用了主线程和电梯线程的双线程模式,将输入放在主线程中,在主线程中进行电梯结束的判断,在电梯类中设置特定结束方法。
第二次作业:我为输入设计了一个单独的线程,每部电梯单独拥有一个线程,在主线程中创建输入和电梯线程,在调度器中设置静态变量的输入结束标志,仍在主线程中判断并结束电梯线程。
第三次作业:在上一次的基础上,将ABC三部电梯的创建放在主线程中,将临时电梯的创建放在输入线程中,输入线程将新增电梯返回给主线程,仍在主线程中结束电梯线程。 对于换乘需求,由于本次作业的换乘需求都可以通过一次换乘达到,我在调度器中新建了换乘队列,在有电梯申请请求时判断是否可通过此电梯进行换乘,并为此请求设置换乘层,在从换乘层出来后,将换乘请求加入主请求队列中。
二、扩展性分析
吸取了上一单元的教训,在本单元三次作业的过程中,由于我在设计之初便考虑着电梯的扩展性,而且后续作业的方向也比较好推测,我并没有进行重构,只是每次都加了一些属性或方法来实现新的要求。
代码行数变化:254→445→796
SOLID | My Project | |
---|---|---|
SRP | 单一责任原则 | 调度器进行需求调度,输入只负责获得请求,电梯只负责电梯的运行即出入,基本符合 |
OCP | 开放封闭原则 | 三次作业的递进过程中,为了方便,加以习惯因素,都是采取在原有类中增加新方法的方式,没有很好地贯彻此原则 |
LSP | 里氏替换原则 | 第三次作业对电梯类进行了扩展,没有违反此原则 |
ISP | 接口隔离原则 | 没有实现接口 |
DIP | 依赖倒置原则 | 所有类都依赖调度器类 |
三、程序结构分析
由于本单元的结构在第二次作业并没有改变,故直接贴出最后两次作业的项目UML类图。
在第三次作业中,由于将电梯分为ABC三类,故将原电梯扩展了三个子类,在每个子类中分别设置其特有属性(容量、可停靠层等),并新建了Person类。之前的两次作业一直在采用嵌套的ArrayList来储存需求信息,但第三次作业需求的属性更多了,同时也为了满足老师上课所讲的显式表达原则,故改变了原来的表示方式,这个的改动非常简单,我也可以明显感受到了此改变所带来的操作的方便性。
至于线程之间的协作关系,画出来的简单UML协作图如下
关于各个方法的复杂度分析如下(略去了一些简单的get和set方法),可以看出,大部分的方法复杂度都不是很高,只有调度器中的几个方法有较高的耦合度和复杂度,这可能归咎于优化时增加的一些判断方法来减少电梯的开关门次数,目前没有太好的解决办法
Method | ev(G) | iv(G) | v(G) |
---|---|---|---|
Controller.Controller() | 1 | 1 | 1 |
Controller.add(PersonRequest) | 2 | 3 | 3 |
Controller.addTransQueue(Person,int,char) | 2 | 3 | 4 |
Controller.addTransferPerson(Person) | 1 | 2 | 2 |
Controller.canInDirection(char,Person) | 3 | 2 | 3 |
Controller.canInFloor(char,int,Person) | 1 | 2 | 2 |
Controller.canTransferInFloor(int,char,int,Person) | 3 | 3 | 3 |
Controller.getDirectArrive(int,int) | 4 | 1 | 4 |
Controller.getInstance() | 1 | 1 | 3 |
Controller.getPeopleIn(int,int,char,int) | 10 | 8 | 12 |
Controller.getTansToFloor(char) | 4 | 3 | 4 |
Controller.getTransferFloor(Person,char) | 8 | 4 | 8 |
Controller.havePeople(int,int,char) | 8 | 6 | 8 |
Controller.havePeopleIn(int,int,char) | 7 | 6 | 8 |
Controller.isEmpty() | 1 | 4 | 4 |
Elevator.Elevator(String) | 1 | 1 | 1 |
Elevator.arrive() | 1 | 1 | 1 |
Elevator.changeDirection() | 4 | 4 | 7 |
Elevator.closeDoor() | 1 | 1 | 1 |
Elevator.elevatorRun() | 2 | 1 | 3 |
Elevator.end() | 1 | 2 | 3 |
Elevator.getPeopleSize() | 2 | 2 | 3 |
Elevator.isEmpty() | 1 | 2 | 2 |
Elevator.isNeedOpen() | 4 | 5 | 8 |
Elevator.isNeedWait() | 9 | 7 | 14 |
Elevator.open() | 1 | 2 | 2 |
Elevator.openDoor() | 1 | 1 | 1 |
Elevator.peopleIn() | 1 | 5 | 5 |
Elevator.peopleOut() | 1 | 4 | 4 |
Elevator.run() | 4 | 6 | 9 |
Elevator.waitWorking(Integer) | 1 | 1 | 2 |
ElevatorA.ElevatorA(String) | 1 | 1 | 1 |
ElevatorB.ElevatorB(String) | 1 | 1 | 1 |
ElevatorC.ElevatorC(String) | 1 | 1 | 1 |
Input.Input() | 1 | 1 | 1 |
Input.addElevator(ElevatorRequest) | 1 | 2 | 4 |
Input.getElevators() | 1 | 1 | 1 |
Input.run() | 3 | 5 | 6 |
Main.main(String[]) | 1 | 4 | 4 |
Person.Person(int,int,int) | 1 | 1 | 1 |
Person.equals(Object) | 3 | 1 | 8 |
四、bug及互测分析
第一次作业比较简单,顺利通过了所有强测点,也没被别人hack到,只是可能由于我只会让与电梯运行方向相同的人上电梯,也是考虑到以后可能会限制电梯容量,性能分不是特别高,现在想想第一次还是应该让所有人都上电梯,后面再改。
第二次作业,由于我测试不充分,在优化会有多部电梯对同一请求开门时,我采取了判断电梯是否在当前层开门时,提前将请求取进电梯的预约序列的方式,但由于我在过程中有多次判断,导致了后来的请求会把之前的请求覆盖的bug,因为这个bug,我成了全屋唯一被hack的人。。。虽然bug很好解决,但造成的损失也是巨大的,给我敲响了警钟。还有就是出现概率很低的死锁问题,我被仅有一条请求的输入hack到了,但是本地并无法复现,在bug修复时也直接通过了,所以我并没有深究,可能这也导致我在第三次作业的bug。
第三次作业,我被hack到了多个RTLE的问题,就是死锁了,本地复现率也很高,经过我多次尝试, 发现竟是我的结束判断有问题!我一开始是在结束时产生interrupt信号将wait中断,导致了我在第三次作业有一个循环并不会被interrupt断掉,而在出了while循环后错过了中断时机导致陷入无限的等待中,基于此问题,我只能新增了一个interrupt变量,通过进行变量的判断来选择什么时候出循环、结束线程等。
在hack别人的道路上, 我走得也很艰辛。首先,其它同学的代码逻辑有些与我完全不同,读懂别人代码后我就处于一种很懵的状态了,试了一些自己觉得模糊的点却也发现人家的代码都是对的,有些性能不是很好但我也hack不到,考虑到这次数据的复杂性, 我不认为评测机能起到很好的作用(第一次我也每靠评测机hack到别人,反而是自己构造的数据更容易hack到别人),所以我没有花时间去建评测机。
其次,我尝试了一些我认为可能会出现调度问题的测试点,但可能我的想法还是不够全面,我在这个方向上也没有hack到别人。关于死锁问题,直到最后,我才意识到是怎么一回事,更加无法通过这个去hack别人。
总之,这次的互测我是比较失败的,没有很好get到这次互测的精髓所在,这和我不扎实的多线程的知识有关,只想着通过设计策略去hack别人而没有利用多线程的程序特点,还没有从第一单元走出来。
五、心得体会
在本单元的设计中, 我没有特意去采取高超的算法去优化,也没有去对每种换乘策略进行设计,只是做了一些必要的优化,比如第二次作业我的所有电梯都会跑向有需要的楼层,但只有一台电梯会开门 ,这是考虑到需求所在楼层产生的随机性,所以并没有只让一台电梯去接人;第三次作业,我也仅是在电梯申请需求时,判断换乘需求是否可以通过此电梯进行换乘,并没有提前为每位乘客设计最优的换乘策略,这也是考虑到防止有些电梯过忙而延长乘客等待时间,得不偿失。总之,虽然我的优化工作很少,但也是在合理的考虑之下的,虽然肯定比不上优化到极致的大佬,但我也发现我的策略最后得到了较高的性能分,这也印证了随机是个好东西。