BUAA_OO_第二单元作业总结
BUAA_OO_第二单元作业总结
总体概述
本单元的主要任务是模拟多线程实时电梯系统,经过三次作业迭代开发出一个包含横向电梯与纵向电梯,可以实时添加乘客与电梯,根据乘客的出发地与目的地选择合适的路线来运送乘客的电梯系统。在开发过程中熟悉线程的创建、运行等基本操作,熟悉多线程的设计方法,掌握线程安全知识并解决线程安全问题,同时在架构上围绕线程之间的协同设计层次架构,掌握线程之间的交互,强化线程之间的协同层次架构设计。
第五次作业
1、题目概述
本次作业需要模拟一个有五座楼,每座楼配备一部电梯,可以实时添加乘客的多线程实时电梯系统,初步体会多线程程序的设计方法。
2、类图
3、时序图
4、作业思路分析
总体架构
总体架构为“生产者-消费者模型”。虽然本次架构简单,只有五座楼的乘客请求,但我仍写了 Schedule
类作为调度器,方便后续迭代开发。整体框架为 InputThread
作为输入线程获取外部请求并将请求添加至 WaitQueue
外部请求队列中,Schedule
从 WaitQueue
中获取外部请求并分配至每座楼层配备的内部请求队列 PeopleQueue
中,电梯 Elevator
从内部请求队列中获取请求并完成运输。
同步块设置
本次的共享对象为外部请求队列 WaitQueue
和内部请求队列 PeopleQueue
。因为本次外部请求较简单,这两种队列用的是同一个类创建的对象。该类中的所有方法都加了锁,并且全都加了notifyall
。这个做法在第一次作业里是没有大问题的,但实际上并不是所有方法被调用都要进行唤醒线程,这为第二次作业的bug埋下了伏笔。
电梯思路
本次作业电梯使用ALS策略进行调度,开关门根据纸片人特性在发出开门指令后休眠0.4s再进行上下人和关门操作。
- 主请求选择规则:
- (1)、如果电梯中没有乘客,将请求队列中到达时间最早的请求作为主请求
- (2)、如果电梯中有乘客,将其中到达时间最早的乘客请求作为主请求
- 被捎带请求选择规则:
- (1)、电梯的主请求存在
- (2)、该请求投喂的时刻小于等于电梯到达该请求出发楼层关门的截止时间
- (3)、电梯的运行方向和该请求的目标方向一致
5、测试环节与Bug分析
Bug分析
本次作业有很多人忘记关注输出的线程安全导致输出时间戳不是递增的,这个bug我自己也犯了,并且在互测环节与房间里的人互相伤害,一时间战况惨烈。不过除了这个共性bug外,我还找到了房间里面有人电梯超载,和线程无法结束两个bug。电梯超载是该程序进人的某个环节忘记判断电梯当前载客量导致的,线程无法结束是电梯运行策略出了问题导致电梯循环往复运行却不接收乘客导致的。除开我发现别人的bug,在中测结束之后我通过测试发现自己的程序的开门判断逻辑与进人逻辑不符,导致可能会出现开了门却不操作的情况,这个bug在强测环节导致了性能分的丢失,在互测中也被人通过阅读程序的情况发现并且通过构造针对数据造成了tle的错误。
测试
测试方案选择的是单部电梯边界强测数据 + 多部电梯边界数据 + 随机生成的大数据进行测试。根据本次题目要求,单部电梯测试关注电梯超载,进出人判断,捎带判断,主请求判断进行针对测试。由于电梯之间没有交互,多部电梯测试数据思路同单部电梯,只是将这些数据分发给多部电梯进行测试。随机大数据主要测试程序能否正常结束以及是否有没有关注到的问题。在互测阶段使用自己测试过的数据进行hack。
6、总结
本次作业是对多线程的初步探索,我对这次架构的实现较为满意,这个架构一直沿用到了第三次作业也没有进行大的变动。然而在测试环节我犯了两个错误,一个是没有关注输出线程安全问题,一个是发现了开门判断逻辑异常后没有及时修改,导致性能分的丢失。当时我发现异常后修改完重新提交时发现运行时间反倒比修改前长,这个问题困惑了很久,经过反复测试后发现时因为投喂程序存在延迟,导致出现了电梯刚离开又来了新的乘客的情况,而错误的程序因为开门问题导致经过该楼层时耗时比正确程序慢一点,恰好捎带成功,导致最终耗时反倒比正确的程序短。而我因为纠结性能问题最终没有修复这个bug,反倒让强测因此丢失了性能分。
第六次作业
1、题目概述
本次作业相较上次作业新增了横向电梯,要求允许出现跨楼座同层移动的乘客,请求中新增电梯请求,可以根据请求在对应的楼座或楼层增加横向或纵向电梯,要求在第一次作业的基础上,掌握线程安全知识并解决线程安全问题,同时在架构上围绕线程之间的协同设计层次架构。
2、类图
3、时序图
4、作业思路分析
总体架构
本次架构沿用了上次作业的架构,在调度器 Schedule
中增加了应对电梯请求和横向运输乘客请求的处理。同楼座或同楼层的电梯共享一个请求队列。
同步块设置
本次作业有两种共享对象 RequestQueue
类和 PeopleQueue
类,同步块同第一次作业,加在该类下的所有方法上,并在每个方法结束时发出 notifyall
,这也是本次作业的bug根源。
电梯思路
纵向电梯沿用了第一次作业的ALS策略并修复了上次作业开门逻辑与进人逻辑不匹配的bug,横向电梯我考虑到运行路径大都较短,对性能呢给的影响较小,所以也采用了简单的类似ALS的基准策略。对于多部电梯我采用的是自由竞争的分配策略,多部电梯共享同一个请求队列并根据请求队列来决定目前的运行策略。
横向ALS
- (1)、目标方向为能够完成这个请求的最短路线所走的方向
- (2)、分为主请求和被捎带请求两个概念
- (3)、主请求选择规则:
- a、如果电梯中没有乘客,将请求队列中到达时间最早的请求作为主请求
- b、如果电梯中有乘客,将其中到达时间最早的乘客请求作为主请求
- (4)、被捎带请求选择规则:
- a、电梯的主请求存在
- b、该请求投喂的时刻小于等于电梯到达该请求出发楼座关门的截止时间
- c、电梯主请求的目标方向和该请求的目标方向一致
5、测试环节与Bug分析
Bug分析
本次作业在强测时挂了一个点,Bug为 cpu run time error
。发生的原因是在共享对象中每个方法都会进行 notifyall
,这导致有多部电梯遇到队列为空但没结束时就会进入类似轮询的状态。A电梯调用方法查询是否需要结束时判断为需要进入阻塞状态,然后 notifyall
就会唤醒B电梯的阻塞状态,但此时队列仍然为空且未结束,这导致两部或多部电梯反复调用查询函数不能正常阻塞。解决方法为针对每个阻塞情况设定对应的唤醒方法,而不是无脑唤醒。
以下是相关部分代码
//Elevator部分
if (passengers.isEmpty() && processingQueue.isEmpty() && processingQueue.isEnd()) { //查询是否结束
return;
}
if (direction == 0) {
toFloor = processingQueue.nextFloor(floor); //这个函数判断是否需要阻塞
if (toFloor == -1) {
continue;
}
direction = (toFloor > floor) ? 1 : (toFloor < floor) ? -1 : 0;
}
//PeopleQUeue部分
if (peoples.isEmpty() && isEnd) {
return -1;
}
if (peoples.isEmpty()) { //队列为空且未结束,进入阻塞状态
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//不应进行唤醒的查询函数
public synchronized boolean isEnd() {
// notifyAll();
return isEnd;
}
public synchronized boolean isEmpty() {
// notifyAll();
return peoples.isEmpty();
}
本次进入了b房,导致其他同学的代码bug更简单也更容易发现,互测过程测出了超载,无法结束,ctle等bug。
测试
本次测试思路与上次大致相同,因为不存在换乘,横向电梯与纵向电梯之间是相互独立的,除了上次作业的测试思路外外新增了电梯请求测试+同楼座或楼层多部电梯测试+横向电梯测试。但是这些测试是没有办法发现cpu运行时间的问题的,只有是用JProfiler这类工具才能发现cpu运行的问题。
6、总结
本次作业在第一次作业的基础上,更多关注掌握线程安全知识并解决线程安全问题。虽然在写程序的时候没有完全关注到notifyall
的问题,但在bug修复环节还是理解此类bug的发生原因和调试方法了,也算是亡羊补牢为时未晚。总体来说本次作业虽有遗憾,但也学到了不少线程安全问题和测试方法,还是有不小的收获的。
第七次作业
1、题目概述
本次作业要求在前两次作业的基础上实现电梯换乘系统以及自定义电梯,掌握线程之间的交互,强化线程之间的协同设计层次架构。
2、类图
3、时序图
4、作业思路分析
总体架构
本次作业要求实现换乘,因此在上次作业的基础上我增加了 Controller
对乘客进行统一的调度,无论是新来的乘客还是出电梯的乘客都会进入 Controller
判断是否已到达并分配往下一个待乘队列。除此之外,因为前两次作业电梯结束的标志都是外部输入结束,而本次作业存在外部输入结束但内部仍在换乘的可能,因此加入了 PeopleCounter
对进出乘客人数进行统计,只有在外部输入结束且 PeopleCounter
记录所有乘客都已送达的情况才宣告结束。这两个部件都是使用单例模式创建的。
同步块设置
本次同步块修复了上次的bug并且在本次新增的同步块阻塞中也注意了 notifyall
的对应使用,因此本次作业没有出现线程安全问题。
本次作业在调度器中新增了以下同步块来阻塞判断在输入进程结束后乘客是否全部送达。
while (true) {
synchronized (PeopleCounter.getInstance()) {
if (!PeopleCounter.getInstance().isFinish()) {
try {
PeopleCounter.getInstance().wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
break;
}
}
}
电梯思路
本次要求允许自定义电梯,因此修改了电梯的构造方法,经过前两次的测试听说 LOOK 策略比 ALS 策略的性能更好,因此原本想更换调度策略的,然而在调试过程中发现换用新的策略有可能会导致不可知的新bug,最后阶段还是全部换回了前两次的调度策略,虽然性能分跟其他同学比起来确实稍低,但平稳的度过了强测和互测也还能够接受。因为可能出现换乘情况,所以在outpassenger
方法中不是想前两次作业中直接remove,而是将乘客发送给 Controller
进行发配。
换乘策略
在开始写作业前我参考过一些往年的博客,也与周围的一些同学交流过,发现如果真要追求极致的性能,可以使用贪心算法对电梯运行模拟,用加权最短路算法寻找较优的调度策略。但这些方法实现起来太过复杂,即便实现了也可能暗含很多的bug,并且这个单元从一开始就强调电梯调度是不存在最优的调度策略的,本单元的重心一直是放在线程安全这方面的,因此我没有大费周章的实现这些算法,而是根据基准方法用 Controller
进行静态调度。
-
每次运送时先由起始楼座纵向电梯把乘客运送到中转楼层,在中转楼层经横向电梯运输到目的楼座后,再由目的楼座纵向电梯运送到目的楼层。
-
乘客的起始楼座为P,目的楼座为Q,起始楼层为X, 目的楼层为Y,则中转楼层m 满足如下公式:
存在
((M >> (P -'A')) & 1) + ((M >> (Q -'A')) & 1) == 2
且使∣X−m∣+∣Y−m∣
最小的m
5、测试环节与bug分析
Bug分析
这次作业出现了类似第一次作业的bug,并且比第一次作业还要严重。因为横向电梯有限停楼层,所以无论是运行策略还是接人策略都必须考虑到对这个进行判断。然而我有一处判断忘记改了,导致出现横向电梯接到无法送达的乘客的问题。并且这个bug一开始完全没有测试出来,知道距离截至还有半个小时的时候通过大数据测试发现了,最后及时修改,躲过一劫。在互测阶段,虽然是A房但房内仍有一些问题,我用发现bug的那组数据去提交hack了三个人,导致我一度认为进入了低等级的房间,最后发现是虚惊一场。房内其他同学的bug基本上都是换乘接送客出现的问题,有的是跟我类似的问题,还有人没考虑到同层移动也要进行换乘的情况。
测试
本次新增了自定义电梯和换乘需求导致测试要考虑的边界情况比前两次大大上升,我没能考虑到所有的边界情况,也导致最后差点没能发现bug。在前两次的基础上,我对简单换乘+限停不同楼座的横向电梯竞争+多部横向电梯换乘策略进行了测试,不过最后还是靠随机大数据才成功发现了bug。
6、总结
本次作业在前两次的基础上要求掌握线程之间的交互,强化线程之间的协同设计层次架构。我对换乘策略的实现相对满意,虽然性能略弱,但程序安全性和正确性还是得到了保证,这也是我经过前两次作业取得的进步。虽然最后时刻突然发现bug很惊险,但总算是有惊无险的度过了。这次作业也集成了本单元的所有需要掌握的要点,既需要保证线程安全,又要对多个线程之间进行交互,很好的锻炼了多线程开发的能力。
心得体会
经过本单元的学习,我掌握了多线程程序基本开发方法,并且学到了许多线程安全问题以及维护线程安全的方法。不过虽然在第三次作业的时候我使用了单例模式的开发方法,但是还是没能使用工厂模式进行电梯的创建,这是我这个单元作业比较遗憾的地方。如果使用工厂模式对电梯的运行策略,特征属性进行抽象,这样电梯实现的耦合度会降低,能够更好的对问题进行分析,也可以通过选择不同的调度策略来比较那种策略更优。这样的设计才更有层次化,也更“面向对象”。
在线程安全方面本次作业在找bug测试环节也与线程安全打了不少交道,这让我对同步块、多线程交互、阻塞与唤醒等知识的理解更深刻了,也学会了许多针对这些问题的测试调试方法,可谓是受益匪浅。