架构设计
第一次作业
整体架构
参考实验代码, 整体架构为inputThread——inputHandler——elevator
: inputThread读入request, inputHandler分发request给对应的六部电梯, elevator具体执行request请求
共计开启八个线程, 两个线程用于读入并分发Request, 六个电梯线程用于实现电梯行为
look算法
-
到达某楼层时,首先判断是否需要开门
- 判断能否下人: 电梯中是否有人到达楼层为此楼层。 能否上人: 电梯中仍有剩余容量且等待队列中的人目标方向与电梯运行方向一致。 若能直接开门
- 若电梯人全下, 无同方向上人, 电梯运行方向无人等待, 反转电梯运行方向时在此层有人上, 则reverseOPEN; 其余情况, OPEN
-
若不能开门, 判断电梯中是否有人
- 若有人, 则MOVE
- 若电梯中没人
- 若等待队列中无人, 则WAIT
- 若等待队列中有人, 且起始楼层位于电梯运行方向之前, 则MOVE
- 若其起始楼层与此楼层一致但目标方向与电梯运行方向相反, 则reverseOPEN
若上述条件均不成立, reverseMOVE
第二次作业
乘客分配
若电梯处于reset状态、 或即将进入reset状态, 不分配乘客给此电梯。 若六电梯均处于此状态, 则不分配, 等至reset结束后分配进某一电梯
分配规则如下:
- 若有等待状态下的电梯, 且电梯楼层与乘客FromFloor相差小于5, 则分配
- 如果可以顺带, 即顺带后电梯沿此方向运行次数不增加, 则分配
- 如果有电梯处于wait状态, 找到一与当前起始楼层距离最短的电梯、 结合电梯speed, 分配
- 其余情况, 根据电梯沿上或下最多运行次数、 speed, 进行调度
新增resetRequest指令
-
RequestQueue类
新增allRequests、 resetRequests队列, 前者用于InputHandler中输入流的读入, 后者用于elevator电梯接受reset请求的实现。 -
InputHandler类
更改此线程结束的条件, 保证每个elevator在reset后吐出的乘客能被再次按策略分配到elevator中。
具体规则:- 当输入流结束后, 在InputHandler中开始判断每个elevator是否处于等待状态(即电梯中没人、 personRequests为空、 不处于reset状态)
- 若均处于等待状态, 为inputQueue的
finalEnd
赋ture, 否则赋false - 当
inputQueue.isEmpty() && inputQueue.isFinalEnd()
时, 结束InputHandler线程
第三次作业
新增DoubleCarResetRequest指令
-
Elevator类
将原本的Elevator当作父类, 由 NormalElevator、 DoubleCarElevator 继承。 -
NormalElevator类
- 当NormalElevator收到
DoubleCarResetRequest
指令时, 进行同第二次作业一致的reset行为, 清空电梯内、等待列表中的乘客 - 在完成上述操作后, 新建两个DoubleCarElevator、 两个waitQueue, 并添加至elevators、 processingQueue中, 用于InputHandler中的乘客分配。
- 而后
start()
启动DoubleCarElevator, 此电梯线程终止
- 当NormalElevator收到
-
DoubleCarElevator类
- 由于其不会再收到reset请求, 因此只需完善同第一次作业时的电梯功能即可: 使用look策略, 倘若运行至换乘楼层, 需要及时掉头, 同时将电梯中未到达目的地的乘客吐出, 重新分配
- 需要注意的是, 由于双桥箱电梯不能同时位于换乘楼层, 故在一开始创建两个DoubleCarElevator时实例化一个
DoubleCarElevatorLock
, 二者同时拥有这个lock。 - 当电梯即将运行至换乘楼层时, 尝试获得lock, 执行的原子操作为: 抵达换乘楼层, 清空电梯中所有人, 离开换乘楼层。 以上行为结束后, 释放lock。 这样双桥箱电梯便只有一个能抵达换乘楼层, 并在抵达后及时离开, 避免二者相撞
乘客分配
基本上沿用第二次调度的策略。 若不考虑reset情况, 对于一个PersonRequest, 总有六部电梯能携带此乘客(即沿乘客目标方向移动至少一楼)。
策略如下:
- 遍历elevators
- 从电梯列表elevators中深克隆每一个电梯, 找到其中符合要求的成员: 电梯能携带乘客成功移动、 电梯不处于reset状态。 此外, 若电梯运行周期数较多时, 说明此时待分配的乘客较多, 不符合要求
- 将符合要求的电梯放入availableElevators中
- 若其
size() < 2
, 说明此时不符要求的电梯过多。 先将此请求放入bufferQueue中, 线程sleep(100), 重新分配。
为elevators加锁, 避免后续DCResetRequest到来时, 对elevators容器的改变, 导致竞争而报错
- 选择合适的电梯分配乘客
调度优先级: 双桥箱电梯(一次能把乘客送至目标楼层) 等待 > 双桥箱电梯(一次能把乘客送至目标楼层) 能携带乘客 > 普通电梯 等待 > 双桥箱电梯(不能一次能把乘客送至目标楼层) 等待 > 普通电梯 能携带乘客 > 双桥箱电梯(不能一次能把乘客送至目标楼层) 能携带乘客。 其余情况, 随机调度
最终架构分析
UML类图
第三次迭代后的代码UML图如下, 采取PlantUML工具作图
UML协作图
一些细节
bug
第一次作业
第一周刚开始上手多线程, 此次作业难以复现的bug成为痛苦的根源, 且debug方法也不熟练。 过中测时主要有两个bug:
- 使用Hashmap作为电梯中乘客容器时总会导致有部分乘客无法正常乘坐电梯, 换为Arraylist后解决
- 产生死锁: 输入结束时InputHandler仍在wait。 忽视了getOneRequestAndRemove中的wait, 而在inputHandler类中又新增一个wait, 导致线程无法正常结束
强测、互测均未出现bug
第二次作业
由于此次作业刚好与清明时间冲突, 在周四过中测后便不再改进代码, 导致强测错了一个点, 后续的互测变成经验包。 问题如下:
- 未考虑六部电梯均在reset时的情况
- 当其余五部电梯处于reset状态时, 将所有乘客分配给一部电梯
- 调度策略的具体实现细节出错
- 未严格实施类的封装, 导致Input和Elevator共享一个类, 造成功能混淆、代码可读性差
第三次作业
互测出现一个bug:
- 在收到DCResetRequest时, 若此指令为输入的最后一条, 则可能在InputThread输入流结束后, InputHandler提前为所有电梯的waitQueue赋end属性。 而此时有一台电梯正处于DCReset状态, 新增的DCElevator的waitQueue无法再被赋为end状态, 其处于一直wait的状态, 死锁导致RTLE
心得体会
线程安全
主要涉及分发器线程与输入流中国InputQueue、电梯线程与分发器线程中waitQueue的读与写, 采取的是对方法加锁的策略, 保证只有一个线程能访问此对象。 在第三次作业中, 还涉及到对elevators容器的更改, 采取在读或写时, 对elevators上锁的策略。
层次化设计
第一次上机主要就是参考实验代码, 实现inputThread——inputHandler——elevator
的设计。第二、三次同往届相比变化较大, 因此也没有过多参考往届博客的内容, 大部分架构都是靠自己一步步实现的。 第二次迭代基本上沿用第一次架构, 在InputHandler
类中进行乘客分发; 第三次迭代将前两次的Elevator
作为父类, 实现两种电梯的继承……
写在最后
到这里Unit2就要划上一个句号了。 虽说中间夹了个清明, 导致一些bug和卷性能的设计没有完全实现, 这个单元的强测分也普遍不高, 但从这单元刚开始的茫然无措到后面能独立完成架构, 短短三周让我对多线程有了较为深入的理解, 可以说还是有所收获, 也感谢前三周一直努力写oo的自己吧。