BUAA OO BLOG UNIT2
BUAA OO UNIT2 BLOG
摘要:
Narrowly escape from Elevator Unit......
度过愉快的电梯单元,对多线程的设计有了很深的体会。多线程的设计,主要还是聚焦于多线程设计模式的使用、线程安全、轮询(wait-notify机制)几个方面(电梯调度策略像是买一送一<( ̄▽ ̄)/)
以下针对线程安全、调度器、架构设计、复杂度分析、bug分析、hack策略、预防轮询和心得体会几个方面,做出第二单元总结。
线程安全
线程安全主要集中在线程之间的交互,三次作业,我都选择了使用生产者-消费者模式。
生产者 | 消费者 | 缓冲区 |
---|---|---|
InputThread | Schedule | WaitQueue、RequestQueueBuffer、ElevatorBuffer |
Schedule | Elevator | RequestQueue |
第五次作业:
在缓冲区类中全部使用了synchronize关键字完成共享数据保护。
第六次作业:
由于电梯数量可以增加,同时Synchronized 使用锁少量的代码同步问题,Lock适合锁大量的同步代码。
所以第六次作业的电梯的缓冲区RequestQueue使用了lock进行数据同步,其余的缓冲区依然是使用了synchronize关键字。
第七次作业:
延续第六次作业的设计,只不过是将ReentrantLock锁换成了ReentrantReadWriteLock(第六次作业就可以用,但是没有想清楚读写锁的await-signal机制)。考虑到电梯很多时候会去读取等待队列的状态,读写互斥有助于提升性能。从强测性能分的角度,两次作业都采用自由竞争,但是第七次作业的性能分显著高于第六次✌️。
调度器
在我的架构中,有且只有一个总调度器。
每一种类型(即同楼层的环形电梯或同座的纵向电梯为同一类型)的电梯共同拥有一个等待队列。Schedule是一个线程,负责将总等待队列中的请求分配到每种电梯的队列中。
Schedule线程会和Elevator和InputThread两类线程交互。线程交互使用了生产者、消费者设计模式。
对于InputThread, WaitQueue和Schedule,InputThread是生产者,Schedule是消费者。
对于Schedule, RequestQueue和Elevator,Schedule是生产者,Elevator是消费者。
第五次、第六次作业:
调度器逐个获取总等待队列中的请求,并依据请求,将其分配到对应的电梯等待队列中。
第七次作业:
相较于前两次作业,第七次作业的Schedule增加了路径规划的功能,并依据规划的路径,将请求分配到对应的电梯队列。
电梯策略:
第五次作业:
只有纵向电梯,采用了look算法。
第六次作业:
纵向电梯采用look算法,横向电梯采用ALS策略。电梯之间采用自由竞争。
第七次作业:
整体和第六次作业类似,添加了ALS的基准换乘策略。
架构设计
线程安全和电梯调度策略都已在前文中提及了。架构中还需要特别说明的是Passenger类和电梯类。
纵向电梯和横向电梯实现了一个电梯接口,这样可以放在同一个容器中更方便地被调用。
第七次作业中,新写了一个Passenger类,继承官方包中的PersonRequest类。采用流水线设计模式,给Passenger添加了一个BitSet类型的属性。我的换乘策略是ALS,即乘客最多三次进出电梯,所以BitSet为三位,第0位为在出发座的纵向电梯移动,第1位为在环形电梯中的移动,第2位为在目标楼座纵向电梯中的运动。三个步骤中,出发地和目的地都在变化,所以设置了相应的set方法,并重写了get方法。乘客每次出电梯时,会根据其BitSet属性,判断是否到达目的地。
UML图如下(类中仅保留重要的属性和方法)
时序图:
复杂度分析
类复杂度:
Class | OCavg | OCmax | WMC |
---|---|---|---|
BuildingElevator | 1.93 | 7.0 | 29.0 |
Counter | 1.0 | 1.0 | 4.0 |
ElevatorBuffer | 1.0 | 1.0 | 2.0 |
FloorElevator | 2.65 | 11.0 | 45.0 |
InputThread | 4.5 | 8.0 | 9.0 |
MainClass | 2.0 | 2.0 | 2.0 |
Passenger | 1.0 | 1.0 | 10.0 |
Printer | 1.0 | 1.0 | 1.0 |
RequestQueue | 2.55 | 6.0 | 28.0 |
RequestQueueBuffer | 1.0 | 1.0 | 4.0 |
Schedule | 4.6 | 11.0 | 23.0 |
WaitQueue | 1.4 | 3.0 | 7.0 |
Total | 166.0 | ||
Average | 2.10 | 4.15 | 12.77 |
由上述图表可以发现,输入线程类和调度器类类复杂度较高,主要原因是使用了很多if-else语句去进行请求类型和换乘策略的判断。
主要方法复杂度:
方法 | CogC | ev(G) | iv(G) | v(G) |
---|---|---|---|---|
FloorElevator.run() | 32.0 | 6.0 | 9.0 | 15.0 |
Schedule.routePlan(Passenger) | 21.0 | 7.0 | 13.0 | 15.0 |
InputThread.run() | 19.0 | 3.0 | 9.0 | 9.0 |
BuildingElevator.run() | 17.0 | 6.0 | 8.0 | 11.0 |
RequestQueue.getDirection(Passenger) | 14.0 | 4.0 | 2.0 | 6.0 |
Schedule.run() | 13.0 | 5.0 | 9.0 | 10.0 |
BuildingElevator.passengerOut() | 11.0 | 1.0 | 5.0 | 5.0 |
FloorElevator.passengerOut() | 11.0 | 1.0 | 5.0 | 5.0 |
RequestQueue.getAllPassengers(ArrayList, ArrayList, ArrayList, int, Elevator) | 11.0 | 3.0 | 8.0 | 9.0 |
Schedule.initiatePassenger(Passenger) | 7.0 | 1.0 | 5.0 | 6.0 |
FloorElevator.getDirection(Passenger) | 6.0 | 2.0 | 1.0 | 4.0 |
Total | 209.0 | 114.0 | 177.0 | 205.0 |
Average | 2.64 | 1.44 | 2.24 | 2.59 |
表格中罗列了主要方法的复杂度,可以发现,一般线程的run方法的复杂度较高,尤其是两个电梯类,主要还是因为我没有设置专门的策略类去完成电梯调度。
bug分析
第五次作业:
RTLE: 超时的原因是对指导书的理解有误,误以为一个电梯的行为需要连续输出,所以把开门、下人、上人、关门这四个输出放在了同一个synchroize(TimableOutput.Class)保护的代码块中。
第六次作业:
RTLE: 超时的原因是环形电梯的调度出现了问题。之前环形电梯采用的是look算法,但是出现如下数据,电梯就会一直arrive不会接人。
[0.0]ADD-floor-7-2
[1.5]1-FROM-A-2-TO-B-2
[1.5]2-FROM-B-2-TO-A-2
[1.5]3-FROM-C-2-TO-B-2
[1.5]4-FROM-D-2-TO-C-2
[1.5]5-FROM-E-2-TO-D-2
第七次作业:
RTLE:没有处理好if-else if 的分支判断,结构如下,即无论是否符合条件都会执行remove语句。
if(...) {
...
} else if(...) {
...
}
remove()
hack策略
第二单元几乎不会出现WA的情况,绝大部分为RTLE和CTLE,所以主要针对大数据、电梯策略两方向进行检测。
-
RTLE。在互测数据输入截至时间前,一次性投喂最大数量限制的乘客请求。
-
轮询检测。投喂一些数据后,等待很长一段时间后再次投喂。
-
线程终止。如,以一条增加电梯的请求去结束投喂,试试线程能够结束。
-
环形电梯的look检测。使用bug修复中提及的环形look算法出现的问题。
预防轮询!!!
一开始还不知道轮询是什么。。。。。。直到亲身试错(。_。)
用我现在的理解,轮询就是,队列中没有请求,线程就应该交出锁和CPU的控制权进入等待区,如果没有wait,线程的run方法就会一直向队列发出询问,一直占用CPU。
在我的架构中,主要有三处地方需要预防轮询。
-
调度器从等待队列获取请求。如果等待队列为空,调度器应该wait,有新的请求再notify。
-
电梯从队列中获取请求。如果队列为空且电梯中无乘客,电梯应该wait,队列中有新的请求再notify。
-
第七次作业,调度器查看计数器,计数器不为空且等待队列为空且获取到输入结束时,调度器也应该wait,直到计数器的值发生变化再唤醒。
需要特别注意的是,第七次作业的电梯线程容易出现轮询。原因是队列中有人,但是该乘客的楼层被阻塞不可达,所以判断队列为“空”的方法需要重写。
心得体会
1.Multi-thread = Metaphysics ┑( ̄Д  ̄)┍。
-
BUG十分难找
-
本地测试是正确的,可能到测评机上就会出问题。
-
同样的代码版本,每次提交,测评机报错的测试点可能不一样。
-
互测数据,可能需要几次提交才能把bug hack出来。
多线程bug会迟到,但是从不会缺席💔
2.实际编程时,多线程的设计和单线程的设计存在着一些差异。在考虑类的协同的时候,需要更多的思考线程安全和轮询的问题。
3.虽然多线程bug频出,但是在架构迭代方面还是有所进步的。在第五次作业奠定了一个比较好的架构之后,第六次、第七次作业基本上都是在第五次作业的基础上添加功能。第六次作业添加了一个环形电梯类和ElevatorBuffer,第七次作业添加了一个Passenger类和计数器。良好的高内聚低耦合,极大的减少了每次作业的工作量。(上次blog立下的flag做到了🤟)