前言
第二单元的主要任务是模拟电梯,主要涉及调度、电梯内部运行策略、换乘等,培养多线程编程的能力。
第一次作业
第一次作业的要求较为简单,模拟6部电梯的运作以及调度即可。
分析
笔者参考了上机所给的代码架构(即输入流、调度器、电梯三种线程),采用生产者-消费者模式。
通过输入流(生产者)读入乘客请求,将其加入到总的等待队列(waitQueue)中,由调度器(托盘)对waitQueue进行调度操作,分配到每个电梯(消费者)的乘客请求队列(elevatorQueue)里,由每部电梯执行乘客的运送操作。
调度方面,笔者尝试通过乘客的fromFloor、toFloor、电梯的当前层数、电梯的方向,设计一下调度从而提高性能,但因为感觉太过复杂而放弃,使用random调度:对每个乘客请求取一个1-6之间的随机数,取到多少就是几号电梯去接乘客。
电梯内部,我参考了学长的博客:采用LOOK策略,建立枚举类Advice,通过getAdvice方法分析电梯下一步的动作(move,openAndClose,reverse,over等)。
(笔者傻了,没仔细看官方的jar包,自己设计了Request类,效果和官方给的PersonRequest类的功能相同)
锁
加锁方面,我对RequestQueue类里所有方法进行加锁,因为涉及多个线程对waitQueue(总的等待队列)的读写。此外,对电梯类里涉及elevatorQueue的方法,笔者也选择加锁,方法为synchronized(elevatorQueue)。
调度器设计
笔者尝试写一下调度,即根据当前请求的fromFloor、toFloor和电梯当前的floor、电梯的运动方向,将人放到合适的电梯里,但因太过复杂直接放弃)
笔者最终采用random调度:对每一个请求,在0-5之间random一个整数,取到多少就是elevators.get(num)(电梯数组)这部电梯去跑。性能方面还算可以,主要是笔者自身的代码出现一些问题导致性能分不够高。而且相对自由竞争,耗电量低。
电梯内部设计
笔者参考了学长的博客,采用LOOK策略。
根据得到的策略,让电梯执行相对应的任务:
public void run() {
while (true) {
Advice advice = getAdvice();
if (advice == Advice.OVER) {
break; //电梯线程结束
} else if (advice == Advice.MOVE) {
move(); //电梯沿着原方向移动一层
} else if (advice == Advice.REVERSE) {
direction = !direction; //电梯转向
} else if (advice == Advice.WAIT) {
elevatorQueue.waitRequest(); //电梯等待
} else if (advice == Advice.OPEN) {
openAndClose(); //电梯开门
}
}
}
public Advice getAdvice() {
if (canOpenForOut() || canOpenForIn()) {
return Advice.OPEN;
}
//如果电梯里有人
if (curCapacity != 0) {
return Advice.MOVE;
}
//如果电梯里没有人
else {
//如果请求队列中没有人
if (elevatorQueue.isEmpty()) {
if (elevatorQueue.isEnd()) {
return Advice.OVER; //如果输入结束,电梯线程结束
} else {
return Advice.WAIT; //如果输入未结束,电梯线程等待
}
}
//如果请求队列中有人
if (hasReqInOriginDirection()) {
return Advice.MOVE; //如果有请求发出地在电梯“前方”,则前当前方向移动一层
} else {
return Advice.REVERSE; //否则,电梯转向(仅状态改变,电梯不移动)
}
}
}
UML类图
bug分析
中测时出现问题:电梯上人时要将请求从电梯的请求队列中删除,于是我直接边遍历边删除:
for (Request request : elevatorQueue) {
if (request.getFromFloor==this.floor) {
elevatorQueue.remove(request);
}
}
然后交上去直接报错,显示异常:ConcurrentModificationException,发生并发修改。
后来在助教的帮助下,修改了下写法:先用一个temp数组将请求队列里要上去的人存起来,然后遍历temp数组,每次遍历时删除elevatorQueue数组里的
强测出现一个bug,canOpenForIn方法中没有对elevatorQueue对象加锁,导致冲突报错,电梯没法关门。 不稳定复现
互测时程序被房友拿去跑评测姬,同样存在不稳定复现的情况(random大法),但是房友手下留情没有hack我orz
此外还有一个小问题:
(举例)电梯停在1楼,人从7楼下到6楼,笔者的电梯会从7楼上到11楼然后再下到6楼。没有处理好掉头的时机,导致性能分有损失。
而且这个小bug在笔者写作业的时候出现:笔者最开始没设电梯的顶,所以面对这样的请求,电梯会一飞冲天太空电梯
笔者选择在move方法里修改:如果电梯到达11层且方向向上,则电梯掉头向下;电梯到达1层且方向向下,则电梯掉头向上。懒人修改法其实可以写个方法判断一下
第一次作业总结
第一次作业是我初次接触多线程编程,开始写的时候一头雾水,直到周五上机才提供了一些思路,对加锁、同步等有了一点理解:出现多个线程对同一个对象进行操作时,一定要谨慎判断是否需要加锁。
第二次作业
第二次作业新增了两个功能:
- 增加电梯并自定义初始楼层、速度、容量。
- 维护(删除)电梯,要求电梯在两层楼以内停下,将电梯内人在该层放出去,电梯进行维护,不再接人。
分析
对于以上两种功能分别分析。
- 增加电梯:比较简单,直接将新电梯加入elevators(电梯数组)里,使其可以参与调度,并start即可。在输入流中进行上述处理。
- 维护:本次作业难点。维护电梯要求更为复杂一些,要先将电梯从elevators中移除,使其不再接受调度,然后将电梯里的人放出去,同时也要将调度到该部电梯的等待队列里的人放出去,放回到waitQueue(总的调度队列)中,最后开始维护。
维护时,需要判断电梯内部人的情况:如果电梯内部乘客的toFloor等于当前楼层,则乘客正常出电梯;否则将乘客请求变为从当前楼层到乘客的目的楼层,加到throwPersonRequest中。
笔者在电梯类里增加了Arraylist<PersonRequest> throwPersonRequest
元素,将电梯里的人和等待队列里的人放在这个数组中,在维护时对该方法直接get即可得到踢人队列。(理论上不应该存在什么问题但是笔者出了bug
在输入流中新增移除电梯、得到返回请求队列的操作。
调度器修改
由于电梯数量可变,所以对随机数范围进行修改,改为0-elevators.size()。其余未变动。
UML类图
bug分析
重头戏来了:电梯杀人事件
这个bug其实在笔者写的时候就出现了:电梯维护之后没有返回请求队列。
中测之前笔者进行了一下修改,在接收到维护请求后,延迟500ms,等电梯踢完人之后再get返回队列。然后强测t了两个点(
强测过后的修改bug里笔者把延迟时间修改为400ms,调整了电梯踢人和开关门的时间顺序(先open再踢人sleep(400)再关门),RTLE的问题解决了。
然后又尝试对stderr里面报错的方法加锁,最终得到一个奇怪的bug:当电梯刚好运动到这一层,且刚好接到维护请求时,电梯有几率不会理请求队列里的乘客,开始踢人、维护,踢人之后没有其他电梯再去接请求队列里的乘客,即没有返回正确的请求队列。
这个bug不是很容易复现,但是强测的点确实阴间以至于笔者交了十几次才把所有点都跑对,现在也没弄明白到底要怎么改才能对(投机取巧式改bug不可取
第二次作业总结
第二次作业里电梯维护让我吃了不少苦头,也让我对锁的用法有了一些更深的理解。整体架构仍然沿用第一次作业,并未进行大幅度修改,将自己写的Request换成官方给的PersonRequest。
由于电脑环境原因,笔者在第五次作业里没能使用官方给的投喂包进行评测,把评测姬当成评测工具,所幸bug不多便于修改。
第六次作业里笔者重新配置了下环境,至少能用官方的投喂包进行评测,在改bug时通过手搓数据、分析数据点,也修改了一部分的bug。
多线程修复bug和单线程相比难度提升不少,由于执行顺序的不确定性,一个bug可能会跑好多次才可能跑出来。在下单元里应该尝试使用评测姬。
第三次作业
第三次作业笔者摆了(屑笔者),但是也有一点点点点思考
第三次作业新增功能:
- 控制每个楼层处于服务中的电梯数量和只接人的电梯数量
- 新增的电梯有无法到达的楼层,当初始六部电梯全部维护之后会出现换乘需求
分析&&调度器设计(猜测)
笔者认为本次作业需要修改的部分主要为调度器。
对于每一个乘客请求,先遍历电梯队列里是否有fromFloor和toFloor都可达的电梯并记录这样的电梯的数量(num),然后在0-num之间取random分配请求。
如果没有可直达的电梯,则需要利用图进行路径规划实现换乘,找到路径后将这个请求拆分为几个可达的请求,然后按顺序按电梯执行,将人送到相应位置。
对于新增的第一个功能,笔者考虑新增一个“信号量”作为所有电梯的共享资源,当电梯开门时增加、电梯关门时减少,以此进行控制。
总结
第二单元里笔者干了很多第一单元里没干过的事(互测进AB房,还有摆了没进入互测(?)),总体来说不是很满意,第三次作业应该可以尝试一下而不是放弃。
经历了第一单元的各种摧残之后,第二单元里或许会相对从容一点,面对bug时也没有之前那样慌张。初次接触多线程,笔者对锁、同步块、wait¬ify的理解、应用从一团糟到逐渐熟悉,在网上搜了不少资料、也问了很多同学,经历很痛苦也很有趣。
第一次作业经历了好几次重构,上机之前自己写了一点然后感觉不对推翻了,然后看学长的博客无脑模仿又推翻了,又看了另外一名学长的博客又推翻了,最后看了第一个学长的博客,自己理解、自己总结,一步一步走,完成设计。自己写的东西才是自己的,完全走别人的老路终究不好走。
业经历了好几次重构,上机之前自己写了一点然后感觉不对推翻了,然后看学长的博客无脑模仿又推翻了,又看了另外一名学长的博客又推翻了,最后看了第一个学长的博客,自己理解、自己总结,一步一步走,完成设计。自己写的东西才是自己的,完全走别人的老路终究不好走。
电梯月结束了,听说第三第四单元的oo强度就不会这么高了,希望自己能有所突破,认真对待每一次作业,多做评测,不留遗憾。