北航OO课程 第二单元总结
前言
OO课程中流传最广、让人闻风丧胆的电梯月,在此刻已然落下了帷幕。回首过往三周,从最初对多线程的一知半解,到后来与各种电梯bug的负隅顽抗,再到理解锁的机制后的豁然开朗,在这一单元多线程的学习中我还是取得了很大收获的。
第五次作业
设计要求
- 本次作业要求模拟一个多线程的实时电梯系统
- 该系统具有上下行、开关门的功能,并可以模拟乘客的进出
- 系统初始在1层有6部电梯,每部电梯的编号、速度、开关门时间和限乘人数都已给定
设计思路
- 首先建立三类线程,即输入线程、调度器线程和电梯线程,并创建一个请求队列类
- 在Main类中设置两个共享变量,分别为总的等待队列和每个电梯的等待队列
RequestQueue waitQueue = new RequestQueue();
ArrayList<RequestQueue> processingQueues = new ArrayList<>();
- 通过输入线程将乘客请求放入总请求队列(waitQueue)中
- 通过调度器线程将总请求队列中的请求分配给每个电梯,即将总请求队列中的请求分配给每个电梯的等待队列(processingQueue)
- 通过电梯线程对传入的该电梯的等待队列进行处理
UML类图
策略分析
调度器策略
在hw5的调度器选择上我选择了平均分配的方法,即将请求队列中的第6n+k个请求分配给k号电梯(其中k=1,2,3,4,5,6, n为任意自然数),这种调度策略能够充分地利用每一部电梯,避免部分电梯的过度等待,但这种调度策略处理捎带的能力不强,导致最后的运行性能十分一般。总的来说这是一种实现简单,性能一般的中规中矩的调度策略。
运行策略
在电梯的运行上我采用了LOOK策略,其大致思想如下:
- 在电梯中设置两个请求队列,分别代表在电梯外等待的请求(waitRequests)和在电梯内服务的请求(inElevator)
private final RequestQueue waitRequests;
private final RequestQueue inElevator;
- 如果当前电梯中没人,则从请求队列中获取一个主请求,在电梯去接主请求的过程中不做捎带
if (!inElevator.contains(mainRequest)) {
if (mainRequest.getFromFloor() > currentFloor) { passFloor(); }
else if (mainRequest.getFromFloor() < currentFloor) { passFloor(); }
else { arriveFloor(); }
}
- 如果当前电梯中有人,则一定存在主请求,在电梯向主请求的目标楼层前进的过程中,每经过一层都做两次判断,分别判断电梯中是否有人要出电梯、判断电梯外是否有人要进电梯(即满足捎带功能),在主请求结束后,将mainRequest置为空,为下一个主请求的确定做准备
if (mainRequest.getToFloor() > currentFloor) {
passFloor(); testPick();
} else if (mainRequest.getToFloor() < currentFloor) {
passFloor(); testPick();
} else {
arriveFloor(); mainRequest = null;
}
- 在主请求结束后,如果电梯中有人,则从电梯中获取一个主请求;反之,则从等待队列中获取主请求
if (mainRequest == null) {
if (!inElevator.isEmpty()) {
mainRequest = inElevator.getRequests().get(0);
} else {
mainRequest = waitRequests.getOneRequest();
}
}
bug分析
- 电梯线程退出时应满足等待队列为空、电梯内为空以及主请求为空,否则会导致部分请求未被满足时所有电梯线程都已经结束
- 在检查捎带时应当检查两次,分别检查是否有人进电梯和是否有人出电梯,满足捎带的所有可能情况
- 在乘客进出电梯前应当做一次判断,只有电梯外的人才可以进电梯,只有电梯里的人才可以出电梯
第六次作业
设计要求
- 在上一次作业的基础上,实现电梯系统的动态扩展和日常维护
- 关于动态扩展:新增电梯给定编号、起始楼层、速度和满载人数,不一定和初始的六部电梯的参数相同,新增的电梯也可以进行请求的分配和处理
- 关于日常维护:电梯接收到维护请求时,在当前运动方向上最近的楼层停靠,并将电梯内的乘客在该楼层全部放出,新增的电梯至少在2s后才可以被维护
设计思路
- 在上一次作业的基础上,实现新增电梯和维护电梯两个功能
- 修改输入线程,在接收到一个请求时首先判断请求类型,若是乘客请求则放入等待队列,其他请求则直接在该线程中进行处理
if (request instanceof PersonRequest) {
waitQueue.addRequest((PersonRequest) request);
} else if (request instanceof ElevatorRequest) {
if (!elevators.containsKey(((ElevatorRequest)request).getElevatorId())) { ... }
} else if (request instanceof MaintainRequest) {
if (elevators.containsKey(((MaintainRequest)request).getElevatorId())) { ... }
}
- 修改电梯线程,在每次循环开始前判断电梯是否被维护,如果是则将所有乘客在最近楼层放出并结束此电梯线程
public void run() { //Elevator类中的run方法
while (true) {
if (!operational) { //电梯未被维护时operational为true
stopFloor(); //被维护时置为false
break;
}
...
}
}
UML类图
策略分析
调度器策略
在上一次作业中由于平均分配的调度策略表现不佳,性能远差于自由竞争,因此此次作业我换成了自由竞争的调度策略。通过舍弃调度器线程,让所有电梯共享同一个请求队列,当某一个电梯获取到一个请求时,这个请求就会被从请求队列中去除,避免了同一个请求的多次执行。自由竞争的最大缺点就是耗电量较大,在实际应用中并不是一个很好的调度策略。
运行策略
与上一次作业相同,运行策略仍旧沿用LOOK算法,未做任何改变。
bug分析
- 对于处于等待状态的电梯进行维护,由于电梯线程一直在wait状态而无法正常输出维护完成的信息,在维护请求实现的过程中需要对被维护的电梯进行notify操作
- 对于最后一条指令为维护指令,且恰好维护了最后一部正在运行的电梯的情况,将会使得最后这些乘客无法到达目标楼层,解决方法是在MainClass类中增加一个elevatorToMaintain共享变量,记录正在被维护的电梯的数量,只有当该变量为0时才能结束电梯线程,这样可以避免上述情况的出现
- 在维护电梯时,如果电梯内为空且无主请求,电梯将无法被唤醒,导致最后的运行时间超时,这个bug只需要增加一条分支便可解决,但由于其自身的隐蔽性较强,这个bug很难被发现
第七次作业
设计要求
- 在前两次作业的基础上,电梯系统需要实现更高级的调度功能
- 增加新增电梯的可达性限制,新增电梯只能到达指定的楼层,初始六部电梯的可达性不做限制,即初始的所有电梯均可以到达所有楼层
- 增加电梯系统的调度参数,要求对于任意楼层,处于服务中(即开门)的电梯最多4部,处于服务中的只接人(before ⊆ after,其中before代表开门前电梯内乘客集合,after代表关门后电梯内乘客集合)电梯最多2部
设计思路
- 对于开关门数量限制:应用Semaphore信号量实现,不会出现tle的bug
- 对于可达性限制导致的换乘:采用Dijkstra算法实现动态的路径规划
- 其余部分与上一次作业基本保持不变
UML类图
UML协作图
策略分析
调度器策略
由于自由竞争的随机性太强,容易产生许多不可复现的bug,加大了debug的难度。出于总体的考虑,hw7的调度策略更换回hw5的平均调度,放弃性能转而追求准确性。
运行策略
大致与前两次作业相同,但有部分改动:
- 当检测到某个楼层处于服务中的电梯数量为4或只接人的电梯数量为2时,电梯在该楼层不开门,而是进行等待,等到前面的其他电梯完成服务之后再进行开门服务
- 对每个乘客请求随时规划出到达目标楼层的最佳路径,并将变化实时地反馈到每个电梯的等待队列当中,对电梯的要求有所提高
- 新增电梯对可达性做了限制,因此新增电梯只需要在可到达的楼层进行开关门和可稍带判断,其他楼层只需要经过即可,不需要做过多的判断
bug分析
- 在开门数量限制问题上,使用全局变量会产生tle的bug,使得CPU使用时间超时,换成课上实验提供的信号量的方法可以避免这个问题
- 重复接人问题,由于调度分配的不合理,导致一些需要换乘的请求被多次满足,这个需要提前规划好每个乘客的乘坐线路,并将请求分配给每个涉及到的电梯
总结
同步块的设置和锁的选择
- 同步块有RequestQueue类中对请求队列进行读或写的方法块,包括增加请求(addRequest)、删除请求(removeRequest)、获取请求(getOneRequest)以及获取请求队列(getRequests),还有对判断是否结尾的变量isEnd进行修改的方法块(setEnd),以及其他方法中需要进行唤醒某个线程的操作时使用的synchronized代码块。
- 由于设计到后期已经完全放弃性能,因此未采用读写锁等较为高效的方法,而是全部使用synchronized关键字来修饰所有的共享资源,对于hw7电梯开门数量限制则是采用了信号量的方法。由于本单元的电梯系统中经常要对请求队列进行写操作,因此采用全部synchronized关键字的方法所造成的资源浪费并不是很大。
调度器设计和调度策略
- hw5和hw7采用平均分配的调度方法,hw6采用自由竞争的方法,具体在前文中已有所提及。
- 本单元调度器设计最大的问题就是没有从始至终坚持同一个调度策略,而是想找到一种更优的调度策略,最后反而大部分时间都在做无用功,画蛇添足。
心得体会
- 在多线程的程序中,由于线程运行的不确定性,debug成了一件十分恼人的事情,许多bug的不可复现让人叫苦不迭。
- 线程安全是程序设计时必须要考虑的一个因素,直接关系着程序的稳定性和性能,只有在设计、编码、测试的全过程中都考虑线程安全的影响,才能避免各种无意义bug的产生。
- 电梯这一单元在层次化设计角度我感觉是不如第一单元的表达式解析的,第二单元更多要考虑的是各个线程之间的交互和影响,并未在层次化设计上大费周章。
- 电梯月至此已落下帷幕,多线程的学习也就此告一段落。