北航OO课程 第二单元总结
写在前面
- 这一单元是OO课程中难度最大的一个单元,有学长说一千个人就有一千种OO电梯,我深表赞同,由于多线程的复杂性和灵活性,同学们的架构千奇百怪,导致我们在互相讨论的时候也出现了各种不能兼容的思维问题,所以无论编写程序还是debug难度大大增加。
- 不可否认的是,这一单元也是收获颇丰的一单元,随着迭代的进行,我对多线程的理解不断加深,而且也更能够将问题抽象化、模块化,有了一些自己的思路,在此简单介绍,以供各位批判
第五次作业
设计要求
- 本次作业要求模拟一个多线程的实时电梯系统
- 该系统具有上下行、开关门的功能,并可以模拟乘客的进出
- 本次作业给定乘客搭乘电梯的编号,即乘客自行选择进入哪部电梯
- 系统初始在1层有6部电梯,每部电梯的编号、速度、开关门时间和限乘人数都已给定
思路
- 三个线程类
Input
、Schedule
、Elevator
,分别负责输入、分配请求、电梯运行 - 请求类
Request
、请求队列类RequestQueue
,其中RequestQueue
类充当共享变量,用来实现总请求队列和电梯的请求队列 - 采用
生产者-消费者
模式,Input
为生产者,Schedule
为一级消费者和生产者,Elevator
为二级消费者接受来自Schedule
的请求 - 在
MainClass
主线程中创建总请求队列、输入线程、调度线程以及电梯线程 - 由于请求的电梯编号固定,因此只需要通过调度器将乘客放入该电梯的请求队列即可
- 电梯中包含
Strategy
类,用于决策电梯的下一步动作和是否转向
UML类图
调度策略
- 本次没有电梯调度策略,因为电梯编号固定
运行策略
- 对于单电梯采用
Look
策略,该策略是Scan
算法的优化,能够更合理的运行- 如果前方有请求或者电梯中请求未完成则按当前方向继续运行
- 如果电梯为空且电梯运行方向前方没有请求且本层楼也没有同方向请求则转向
- 只捎带同方向的请求
- 借用往届学长的思路,可以采用
量子电梯
来缩短运行时间- 要求中规定运行时间为
0.4s
即关门到下一楼层的时间为0.4s
,我们可以要求电梯在原地等候,0.4s
后瞬移到下一层即可,这样代表还能处理这期间的请求 - 另外开门使用
0.2s
,关门使用0.2s
,只需要保证开关门之间间隔0.4s
即可,所以我们可以记录上次相关操作的时间,执行完操作后只需要sleep(400 - (currentTime - lastTime))
即可,若操作时间大于0.4s
,则不睡觉直接关门后移动
- 要求中规定运行时间为
- 该运行策略一直沿用至
Hw7
,后面不再赘述
同步块
- 特别提醒,同步控制的锁要加在方法上
- 注意改变队列要
notifyAll()
- 要采用
Get-And-Remove
的方式,即查询后删除,不然可能会出现同时读写问题 - 由于共享变量是
RequestQueue
,所以如果要遍历请求队列一定要在RequestQueue
中,如果取出容器在外部遍历很有可能出现同时读写问题,外部遍历一定要加好同步控制! - 锁本身是访问权,拿到锁才有访问资格
出现的bug
由于没有理解好共享变量的概念,将共享变量中的容器取出后在电梯方法中遍历,出现了同时读写问题
public void whoToPickUP(Elevator elevator) {
ArrayList<Request> requests = requestQueue.getRequests();
for (Request request : requests) {
...
}
} //wrong
在此处设置同步控制即可
public void whoToPickUP(Elevator elevator) {
synchronized (requestQueue) {
ArrayList<Request> requests = requestQueue.getRequests();
for (Request request : requests) {
...
}
}
}//right!
第六次作业
作业要求
- 本次起不再指定乘客进入电梯号,可以自行设计调度策略
- 新增
Reset
指令,接受该指令后电梯重置,修改容量和运行速度
思路
- 整体架构沿用上一次作业
- 新增
Reset
类,该类继承Request
类,用于实现重置请求 - 沿用上次请求处理思路,将重置请求放入请求队列内,若处理请求时检测到重置请求,则立马停止工作开始重置
- 新增
ShadowElevator
类,用于模拟单电梯运行 - 重写调度策略
UML类图
调度策略
- 采用
影子电梯
策略模拟电梯运行从而做出局部最优选择 - 影子电梯采用深克隆来获得电梯当前的请求信息和运行状态,模拟该电梯加入当前请求后的运行时间从而将请求分配给最早能够完成当前所有请求的电梯
ShadowElevator
类实际是Elevator
和Strategy
类的整合- 将运行方法改为一般方法
- 将
sleep(400)
改为runtime += 0.4
- 实际上并不完全准确,可能会出现模拟完后电梯状态发生改变从而决策失误的情况,但我们认为CPU运算速度要远远快于电梯状态改变的速度,因此在绝大多数情况下可以正确模拟
运行策略
沿用上次作业运行策略
同步控制
没有新增同步控制
出现的bug
- 强测没有bug
- 互测出现了bug,由于reset请求的出现,可能会有一个电梯率先完成重置其余电梯仍然在重置导致所有请求分配给了这个电梯,会出现运行时间过长的问题
- 需要限制过多电梯重置时不进行请求分配,等待一小会
第七次作业
作业要求
- 新增
DcReset
请求,将电梯重置为双轿厢电梯,规定A轿厢只能在换成楼层下区运行,B轿厢只能在换乘楼层上区运行,且不允许两个轿厢同时出现在换成楼层
思路
- 整体架构依然沿用上次作业
- 新增
DcReset
类继承Reset
类,用于新的重置 - 新增
DcElevator
类继承Elevator
类,用于实现双轿厢电梯 - 新增
ShadowDcElevator
类用于模拟DcElevator
- 新增
Output
类,封装输出方法,适应不同类型电梯的输出 - 新增
ExFloor
类,充当换乘楼层,作为共享变量存在,控制双轿厢不同时进入 - 新增
ExNumber
类,全局计数功能,用于统计当前未完成请求数,用于判断是否可以结束运行 - 重写调度策略
UML类图
调度策略
- 采用
影子电梯
策略模拟电梯运行从而做出局部最优选择 - 本次与上次略有不同,由于存在换乘策略,在模拟时只考虑全部请求均进入电梯即结束模拟,即请求队列为空则结束模拟
- 另外由于双轿厢电梯耗电量低很多,所以在本次调度中加入对电量的模拟,用数组计算总电量,并按照一定比例将电量与速度进行线性拟合,从而找到比较合理的综合指标
运行策略
沿用上次作业运行策略
其他策略
- 判断是否结束运行
采用了全局计数的方式,如果Input
有新的乘客请求则++
,若请求正式完成则--
,最后判断是否全部完成且输入结束即可 - 防止双轿厢电梯相撞
用ExFloor
类来实现互斥,在每次move
之前判断是否下次进入换乘楼层,若进入则对换成楼层的状态
进行判断,空闲则可以进入并修改状态为占用,离开时修改状态为空闲
出现bug
由于沿用了上次作业的架构,但是计算量翻倍,出现了CTLE
的问题,具体修改方法为在电梯重置时调度器不应该轮询,而应该等待部分电梯重置完成后唤醒线程
心得体会
- 在多线程的程序中,由于线程运行的不确定性,在处理相关bug时会出现不能复现的问题,甚至在修复时可能出现重复提交一次即可通过的情况,debug十分困难
- 线程安全是程序设计时必须要考虑的一个因素,直接关系着程序的稳定性和性能,我们在编程时应该尽力避免线程之间的无意义互动和通信,同时在设计、编写、测试时都要反复考虑线程安全问题,这样才能避免各种无意义bug的产生
- 电梯这一单元的层次化设计并不复杂,总体采用
生产者-消费者
模式,只是随着迭代的进行修改调度策略以及相关的运行策略即可 - 不要过度考虑性能问题,更多的要从本次作业的实现中考虑结构上的、设计上的问题,拥有一个良好的架构才有可能创造出更好的性能