BUAA_OO第二单元总结_2020
OO第二单元的内容是电梯模拟器, 涉及到多线程相关知识. 本文将分别叙述三次作业的设计与测试情况, 最后统一评价三次作业与SOLID原则的适应性, 并于文末略述心得.
第一次作业: 单部电梯模拟
设计策略
我采用了简单的生产者-消费者模式作为代码框架: 主线程启动生产者线程(InputSequence)与单消费者线程(Elevator), 二者共享调度器(Scheduler)队列.
UML类图如下, 已隐藏方法区.
关于线程安全, 我将涉及到非原子性的操作全部进行加锁(以Scheduler对象作为Monitor), 保证线程间信息读写不会产生冲突. 由于有生产者消费者设计模式的指导, 受益于前人的指挥, 我在安全性上没有太多的考量, 主要任务在如何有效停止线程, 如何防止CPU时间超时(避免轮询).
设计出能够有效停止线程的程序是重中之重, 因为只要施行捎带, 理论上不会出现运行时间过长. 这里的问题主要集中在输入线程结束后如何退出线程. 我在共享对象Scheduler中添加了线程安全的信号isInputEnd
, 用于输入线程告知电梯线程输入结束, 在消费者get()
方法的while
循环中, 将循环条件设置为isRequestListEmpty() && !isInputEnd()
.
调度策略:
使用LOOK算法.
结构分析:
-
代码复杂度
method ev(G) iv(G) v(G) Elevator.arriveNextFloor(int) 1.0 2.0 2.0 Elevator.checkHalfway() 1.0 1.0 1.0 Elevator.Elevator(Scheduler) 1.0 1.0 1.0 Elevator.getHalfwayInOut() 1.0 1.0 1.0 Elevator.openAndClose(ArrayList) 2.0 2.0 3.0 Elevator.printInOut(ArrayList) 1.0 4.0 4.0 Elevator.run() 3.0 3.0 4.0 Elevator.upAndDown(int,int) 1.0 5.0 5.0 InputSequence.InputSequence(Scheduler) 1.0 1.0 1.0 InputSequence.run() 3.0 4.0 4.0 MainClass.main(String[]) 1.0 1.0 1.0 Scheduler.addElevatorRequest(PersonRequest) 1.0 1.0 1.0 Scheduler.addRequest(PersonRequest) 1.0 1.0 1.0 Scheduler.checkDirUp(PersonRequest) 1.0 1.0 1.0 Scheduler.chooseBestRequest(int,boolean) 2.0 2.0 2.0 Scheduler.chooseCase1(int,boolean) 4.0 2.0 10.0 Scheduler.chooseCase2(int,boolean) 4.0 9.0 13.0 Scheduler.elevatorEmpty() 1.0 1.0 1.0 Scheduler.floorEmpty() 1.0 1.0 1.0 Scheduler.getInputFinished() 1.0 1.0 1.0 Scheduler.getMainRequest(int,boolean) 2.0 8.0 9.0 Scheduler.setInputFinish() 1.0 1.0 1.0 Total 40.0 63.0 78.0 Average 1.4814814814814814 2.3333333333333335 2.888888888888889 其中两个圈复杂度高的方法, 基本圈复杂度不高, 原因是我在里面嵌套了循环, 基本可以接受. 这两个方法是用于选择调度策略的方法.
-
UML协作图
评测:
强侧和互测没有出现问题. 由于本次作业需求明了, 结构清晰, 没有发现自己逻辑上的bug.
互测时我发现同组的代码有可能忽略最开始的请求, 成功hack两次.
第二次作业: 多部电梯模拟
设计策略:
采用Worker_Thread模式, 本质上就是一对多的生产者消费者模式.
Worker是多个Elevator对象. Client是输入线程. Channel是Scheduler.
仅含有一个线程共享调度队列, 存在于调度器中.
这次作业是在第一次作业基础上加了几行代码, 结构几乎和第一次作业相同. 但是这样的作法造成了若干隐患:
- Elevator类和Scheduler类过于庞杂.
- 难以灵活调度, 限制了电梯整体的运行效率.
- 无法进行进一步扩展.
关于作业增加电梯承载人数限制, 我是将其作为电梯的固有属性来实现.
UML类图如下, 已隐藏方法区:
调度策略:
在LOOK算法的基础上, 我要求每部电梯在捎带时一次不能超过4人进入, 面对新增的请求, 所有电梯同时迎接, 进行抢夺.
这样就会出现在15楼有一个乘客发出来请求, 5部电梯一齐迎接的情形. 不过我放宽了捎带的资格, 允许方向相反的乘客同样进入电梯, 部分弥补了这一缺点.
上述行为的目的, 是让电梯在合理的运行的同时拥有更多的随机性. 从强测结果来看, 性能部分可以说差强人意, 可见随机性对于性能的提升效果还是有的.
结构分析:
-
代码复杂度:
由于大部分方法仅含有1~2行代码, 用于一些线程安全的访问与判断, 或是增加可读性的简化, 因此省略这些方法的度量.
method ev(G) iv(G) v(G) Elevator.getElevatorNum() 7.0 2.0 7.0 Elevator.openAndClose(ArrayList,ArrayList) 2.0 3.0 4.0 Elevator.printInOut(ArrayList) 1.0 4.0 4.0 Elevator.run() 4.0 3.0 5.0 Elevator.upAndDown(int,int) 1.0 5.0 5.0 InputSequence.run() 3.0 4.0 4.0 Scheduler.chooseCase1(Elevator) 4.0 4.0 12.0 Scheduler.chooseCase2(Elevator) 4.0 11.0 13.0 Scheduler.getMainRequest(Elevator) 2.0 7.0 8.0 Scheduler.halfwayEnter(Elevator) 3.0 4.0 5.0 Scheduler.halfwayOut(Elevator) 1.0 4.0 4.0 Total 69.0 95.0 124.0 Average 1.6428571428571428 2.261904761904762 2.9523809523809526 类复杂度:
class OCavg WMC Elevator 2.0 38.0 InputSequence 2.0 4.0 MainClass 2.0 2.0 RequestWrapper 1.0 3.0 Scheduler 3.0714285714285716 43.0 Total <110.0 Average <2.619047619047619 <15.714285714285714 复杂度最高的仍旧是调度相关的方法, 因为里面嵌套了for循环和if-else块. 电梯类和调度器类的复杂度可以明显看出这次作业设计的复杂性. 这似乎预示着重构...
-
UML协作图:
评测:
强侧出现了跪了两个点, 互测被hack了一次.
原因: 逻辑错误: 在捎带的过程中如果出现最底层或最顶层有超过(停留电梯数目*7+2)个数的请求, 那么会产生死锁. 产生的原因是为了优化, 我的电梯有up标志位用于判断电梯的上下行, 而在电梯运行到底层或者顶层时, 我没有及时改变up标志位, 导致小概率会产生思索, 所有电梯全部wait.
解决: 删除优化的那一行代码, 并且添加up位在最底层与最顶层的改变, 作为合并修复解决了所有bug.
第三次作业: 可换乘的电梯模拟
设计策略:
我重构了...
主要改变是拆解了请求, 增加了二级调度队列, 取消了电梯内部队列. 增加了调度的灵活性, 实现了动态的电梯线程与调度器的交互---用于换乘.
由于换乘机制的引入, 不能像前两次作业那样仅凭借主请求队列作为线程结束的标志. 为了减少主调度器的负担, 提高运行效率, 线程间的共享对象除了主调度器以外还有每个电梯所属的副调度器, 用于通知某一电梯其他电梯是否执行完毕.
正因为结束的条件复杂了, 很容易造成死锁或者ctle, 尽管程序看起来像是由notify和wait在主导, 但一运行电脑风扇就使劲转, 打个日志或者print, 就会发现被某一个电梯的信息刷屏了... 有人问我解决办法, 我给出的建议是, 不被死锁的前提下, 尽可能放宽进入wait的条件, 尽可能收缩电梯线程继续运行的条件. 如果代码逻辑清晰, 这些都比较明显.
我发现没有必要位电梯模拟真实的人, 仅凭借请求的执行即可完成所有进出操作与承载量判断, 因此取消了电梯内部队列.
这一次真正实现了Worker_thread模式的一个重点, 也就是参数获取与方法调用的分离, 体现在trueRequest
的execute
方法上, 这样可以增加请求执行的灵活性.
架构如下图所示:
UML类图如下, 方法区隐藏:
调度策略:
由于不同电梯类型可以停靠的楼层有别, 复杂度增加, 因此换成了标准的SSTF算法, 同时进一次增加了分配的随机性. 由于拆分了请求, 能够禁止多部电梯同时迎接一个人这种VIP待遇的发生. 性能上有了一定的改善.
在三次作业中, 调度器一直是以被动的角色出场, 它仅在受到请求时进行分配, 提升调度器主动性的方法有两个, 其一是将调度器作为一个线程, 作为具有能动性的个体, 其二是在电梯线程中合理的位置添加捎带检查, 鉴于前者引入的新的线程, 与worker_thread设计模式不同, 所以我用的是后者.
结构分析:
- 代码度量如下(隐藏一些用于增强可读性的方法):
method | ev(G) | iv(G) | v(G) |
---|---|---|---|
Elevator.checkHalfway() | 2.0 | 17.0 | 18.0 |
Elevator.Elevator(String,String,SubScheduler) | 2.0 | 2.0 | 5.0 |
Elevator.initCapacity() | 5.0 | 2.0 | 5.0 |
Elevator.initDockList() | 2.0 | 2.0 | 5.0 |
Elevator.range2List(int,int,ArrayList) | 2.0 | 2.0 | 3.0 |
Elevator.run() | 3.0 | 5.0 | 6.0 |
MainClass.main(String[]) | 3.0 | 3.0 | 3.0 |
MainScheduler.closestTransit(String,Elevator) | 2.0 | 5.0 | 11.0 |
MainScheduler.isAllEleEnd() | 3.0 | 2.0 | 3.0 |
MainScheduler.isEnd() | 3.0 | 2.0 | 4.0 |
MainScheduler.takeReq(Elevator) | 5.0 | 8.0 | 9.0 |
MainScheduler.takeReqHalfway(Elevator) | 4.0 | 6.0 | 7.0 |
SubScheduler.takeReq(Elevator) | 3.0 | 2.0 | 3.0 |
TrueReq.execute(Elevator) | 6.0 | 8.0 | 10.0 |
TrueReq.isAble2Take(Elevator) | 5.0 | 2.0 | 8.0 |
Total | 105.0 | 136.0 | 179.0 |
Average | 1.75 | 2.2666666666666666 | 2.9833333333333334 |
SSTF的实现有for块和if-else块嵌套的情况, 这个没有分的必要, 基础圈复杂度不高.
由于我将请求拆分, 检查捎带的逻辑变得复杂了很多, 或许我可以将获取捎带列表, 执行捎带, 以及电梯开关三种操作分开.
- UML协作图:
评测:
强侧与互测没有出现问题. 新架构比较稳定.
互测时我hack到了两个人, 都是在同一时刻产生大量请求的情况下出现问题. 具体原因没有深究. 不过这样的bug也很好de就是了.
SOLID:
以第三次作业为主.
-
SRP
-
总调度器负责将指令按调度分配给副调度器;
-
电梯负责执行指令, 开关门.
-
副调度器负责给予电梯合适指令.
-
输入类负责线程创建与发送请求
-
-
OCP
没做到, 没有进行继承上的扩展, 以后需要注意, 我是对修改开放, 对扩展关闭...
-
LSP 略
-
ISP 略
没有进行面向接口编程, 电梯种类单一, 职能全部重叠, 仅仅固有属性不一.
-
DIP
没有进行面向接口编程, 没有涉及这些.
心得
初探多线程, 与OS的进程知识一起学习, 感觉挺爽. 线程安全是或许容易实现, 但是难以兼顾效率与线程安全. 日后涉及的项目若与多线程相关, 肯定不能把整个方法锁上. 所以多线程的依旧前路遥遥, 我需要多加了解相关设计模式, 积累经验.
说实话我认为这单元的难点在合理结束线程运行上, 这需要我清楚梳理与运行逻辑, 考虑多种情况, 如有遗漏便是RTLE或者CTLE, 在应用Worker_thread时多加注意.
这次作业体验依旧不错, 感谢老师助教与同学们的贡献. 期待下一次.