BUAAOO Unit2:电梯作业总结
1. 作业分析
1.1 第5次作业——单部多线程电梯
1.1.1 需求分析
只有1个电梯,有一定容量限制,乘客到达时间随到达模式的不同而异。到达模式分为Random、Morning、Night三种模式。
-
Morning:乘客到达间隔不超过2s,起始层都是底层。
-
Night:乘客在Night 指令到达时一次性全部到达,终点层都是底层。
-
Random:允许任何到达情况出现
1.1.2 主要思路
因为Morning模式乘客的起始层都是底层,所以我的想法是在读入的时候,如果是Morning模式就让电梯等待4s,再开始运人,这样可以避免最开始一次只运1个人的尴尬情况。对于其他Night模式当做Random模式来跑,即没有特殊的调度方法。
我使用了“消费者-生产者”模式,输入线程负责读入要乘坐电梯的乘客,然后将乘客放进缓冲区(WaitingRoom),然后电梯线程从WaitingRoom中读取乘客运人。
1.1.3 主要的类及功能
InputHandler
:读入线程,从标准输入中读取数据,确定当前的到达模式以及读取即将乘坐电梯的乘客
WaitingRoom
:用一个TreeMap来放置候乘的乘客们
Elevator
:电梯线程,电梯线程负责“到达”、“楼层间运行”、“开门”、“关门”、“上人”、“下人”
RunningType
:电梯的运行状态类型,分为:到达某一楼层ARRIVE、开门OPEN、关门CLOSE、楼层间运行MOVE、休息REST、结束END
ElevatorState
:电梯的属性,属于电梯线程的一种性质,里面包括了电梯的运行方向、运行状态类型、电梯当前所处楼层、电梯最大容量、电梯当前容量、最高层、最低层、电梯所携带的乘客
Scheduler
:根据当前电梯的ElevatorState
和WaitingRoom
的情况,决定Elevator
的下一个运行状态。
1.1.4 调度器设计
在完成这一次作业时,我当时认为“调度器”就是实现电梯的调度算法的类。所以我的调度器就是Scheduler
类
我的调度器不是线程,而是决定电梯线程的运行状态,就如很多同学所说的“状态模式”,对我的调度器更像是一个状态模式的抽象,通过利用上学期计组学过的有限状态机,根据这一时刻的电梯运行状态(ElevatorState)和WaitingRoom的情况来决定下一时刻电梯运行状态和下一个目标层。
对于电梯的下一个目标层我采用的调度方法是:LOOK算法
有限状态机的逻辑如下:
调度器和线程的交互
我的调度器只和电梯线程有交互。在Elevator
中的run()
方法中,对ELevatorState
的电梯运行状态类型进行判断,使用了一个switch
语句,根据REST、MOVE、OPEN、ARRIVE、OPEN、CLOSE、END来让电梯sleep相应的时间、并且进出乘客、改变楼层、改变方向、结束线程(即做出相应的动作)。
当做完相应的动作后,再调用Scheduler
来确定下一个状态。
1.1.5 同步块的设置和锁的选择
这一次作业中,我对两个变量的读写取进行了加锁。
第一个当然是候乘表。我采用了“消费者-生产者”模式,所以肯定要维护的就是WaitingRoom
候乘表,也就是“消费者-生产者”模式的缓冲区,所有对WaitingRoom
进行读写取的操作都需要加锁。
-
对
WaitingRoom
有写操作的就是InputHandler
的输入线程 -
对
WaitingRoom
有读操作的是Scheduler
类,因为要根据候乘表的情况判断电梯的下一到达楼层和运行方向 -
对
WaitingRoom
有取操作的是Elevator
,因为乘客进入电梯时需要把乘客从候乘表中删除。
第二个是ElevatorState
。因为电梯线程要根据ElevatorState
中电梯的运行状态种类来做出相应的动作,而Scheduler
要根据ElevatorState
来确定电梯线程的下一个动作。
锁与同步块中处理语句直接的关系
锁只加在需要对上述两个变量的读写取操作上,对于其他的操作不用加锁。
1.2 第6次作业——多部同型号电梯的运行,可以动态增加电梯
1.2.1 需求分析
初始有3个电梯,三个电梯一模一样(有一样的容量限制、可达楼层、运行速度、开关门速度),到达模式与第5次作业相同。
1.2.2 主要思路
最开始我是采用了自由竞争的策略,也就是“集中式”调度,但发现来一个人所有电梯都会朝这个电梯飞奔去,但最终只有一个电梯会接到这个人,这样会太慢、而且在中测中有一半的点会超时。所以我采用了“分布式调度”,为每个电梯分配独立的候乘队列,由分配器器将乘客分配到各个电梯候乘队列当中。
1.2.3 新增类及功能
WaitingList
:实现第5次作业中WaitingRoom
的功能,不同的是每一个电梯拥有一个WaitingList
,作为每个电梯各自的候乘列表。
WaitingDispatch
:拥有所有电梯的ElevatorState
和WaitingList
,根据各个电梯的ElevatorState
和候乘人数,来确定要把请求交给哪个电梯。
WaitingRoom
:管理所有电梯的WaitingList,每次读入PersonRequest后进入WaitingRoom中,然后调用WaitingDispatch来获得要放入的电梯的id,然后把PersonRequest放到对应电梯的WaitingList中。
1.2.4 调度器设计
在完成这一次作业时,我发现我对“调度器”的理解出现了偏差,最开始我认为“调度器”就是实现电梯的调度算法的类,但在这次作业中我发现,“调度器”好像是实现分配每个请求到每个电梯的队列中的东西,也就是这次作业中的WaitingDispatch
。
我的Scheduler
类在这次作业中并没有改变,那么就来讲一讲我的分配器WaitingDispatch
的设计吧
分配原则:
-
找可以捎带该请求的电梯
-
如果没找到,找离得最近的正在休息的电梯
-
如果没找到,找候乘人+电梯内人的总数最少的电梯
调度器和线程的交互
在这次作业中,我的调度器不是一个线程,现在看来应该改成一个线程会更好操作。这次作业中调度器与线程的交互似乎也没有什么交互,那我来说下我的调度器的运作流程吧。运作流程如下:
有一个PersonRequest被InputHandler读入后
InputHandler传给WaitingRoom,WaitingRoom调用WaitingDispatch来确定一个电梯接收这个请求,WaitingDispatch确定后,将电梯的id传回给WaitingRoom,WaitingRoom把请求放到WaitingList队列
1.2.5 同步块的设置和锁的选择
这一次作业中,我对2个变量的读写取进行了加锁。
第一个当然是候乘表。因为我把所有电梯的候乘表都交个WaitingRoom来管理,所以对WaitingRoom中凡是涉及到WaitingList的操作我都加了锁。
第二个是ElevatorState
。和第5次作业一样,在Scheduler和ELevator中加锁,维护单个电梯的ElevatorState,不同的是,我把所有电梯的ElevatorState
都交给了WaitingDispatch
,所以在WaitingDispatch
中还要维护所有电梯的ElevatorState
。
锁与同步块中处理语句直接的关系
与第5次作业相同,锁只加在需要对上述两个变量的读写取操作上,对于其他的操作不用加锁。锁不能不加,也不能随便加
1.3 第7次作业——多部不同型号电梯的运行,可以动态增加电梯
1.3.1 需求分析
初始有3个电梯,三个电梯各自具有不同的型号(容量限制、可达楼层、开关门速度均不同),到达模式不变。
型号 | 到达楼层 | 移动速度 | 限乘人数 |
---|---|---|---|
A | 1-20 | 0.6s/层 | 8 |
B | 奇数层 | 0.4s/层 | 6 |
C | 1-3,18-20 | 0.2s/层 | 4 |
1.3.2 主要思路
可以观察到没有必须要换乘才能到达的请求,所以换乘只是为了能够在更少时间内完成任务。其他实现和第6次作业相同。换乘策略将在1.3.4调度器设计中讲。
1.3.3 新增类及功能
-
MyPersonRequest
:与课程组提供的PersonRequest相同,增加了transferFloor
,为换乘楼层,默认为到达楼层;增加了一个flag标志这个人是不是第一次进入分配器。 -
WaitingToAdd
:作为InputHandler输入线程和分配器线程之间的缓冲区,同时接收来自各个电梯的 -
Elevator
:增加了一个是否往WaitingToAdd
回写乘客的判断,电梯在出人时需要判断这个人的换乘楼层和目标楼层是否一致。如果相同,那么说明这个人已经到达目的地;如果不相同,那么这个人会再次被放入WaitingToAdd
中,等待分配。 -
WaitingRoom
:分配器线程,管理所有电梯的WaitingList,同时设置一个计数器count计换乘人数。
1.3.4 调度器设计
在这一次作业中,我将我的调度器WaitingRoom
改为了线程形式
-
当
WaitingToAdd
中还有乘客等待分配时,进行分配; -
当
WaitingToAdd
中没有乘客,且读入结束,且换乘人数count为0,那么将所有电梯的isReadEnd置为true,结束分配器线程 -
其他情况,休眠,等待WaitingToAdd唤醒
关于计数器count:
-
如果这个人是第一次进入分配器,且换乘层与到达层相同,这说明这个人没有换乘,count不变
-
如果这个人是第一次进入分配器,但换乘层与到达层不同,那么这个人进行了换乘,count++;
-
如果这个人不是第一次进入分配器,且换乘层与到达层相同,说明这个人已经换乘完毕,count--;
-
如果这个人不是第一次进入分配器,且换乘层与到达层不同,说明这个人换乘还没有结束,count不变
分配策略
-
如果可以坐C,优先坐C,如果C的人太多,则顺次判断B
-
如果可以坐B,优先坐B,如果B的人太多,则顺次判断A
-
如果可以坐A,则坐A,如果A的人太多,则换乘
调度策略
根据到达层和起始层之间差的绝对值来判断:
-
如果为1,那么只能坐A
-
如果为2,
-
若是奇数层到奇数层,则坐B;
-
偶数到偶数,坐A
-
-
3到19的情况可以打表输出
PS:在打表的过程中,发现换乘可以递归换乘,所以写起来害比较轻松
调度器和线程的交互
在这次作业中,我的调度器本身就是一个线程,他和输入线程共享WaitingToAdd
,同时又和Elevator
共享每一个自己的电梯候乘表。交互图如下:
1.3.5 同步块的设置和锁的选择
这一次作业中,我对3个变量的读写取进行了加锁。
第一个当然是候乘表。和第6次作业一样
第二个是ElevatorState
。和第6次作业一样,此处不再赘述。
第三个是缓冲区WaitingToAdd
,WaitingToAdd
是存放等待分配的候乘人,所以对它的读写取都需要加锁
锁与同步块中处理语句直接的关系
与第6次作业相同,锁只加在需要对上述3个变量的读写取操作上,对于其他的操作不用加锁。
1.3.6 架构的可拓展性
UML类图
Sequence Diagram
JVM线程的协作图如下:
JVM线程只负责准备好一切工作和启动输入线程、分配器线程、电梯线程。
每个电梯线程拥有一个WaitingList、Scheduler、ElevatorState
输入线程的协作图如下:
因为电梯是可以动态增加的,所以在输入中如果接收到增加电梯的请求,所以红色框出来的就是与电梯线程有关的类
WaitingToAdd
:放收到的乘客请求
MyPersonRequest
:乘客请求
WaitingDispatch
:将新电梯的ElevatorState
添加进去
WaitingRoom
:将新电梯的WaitingList
添加进去
电梯线程的协作图如下:
此部分在之前介绍类的功能时已经阐述过,此处不再赘述。
功能设计和性能设计的Trade off
单论功能的话,我感觉我的电梯还可以拓展很多种电梯,如果再多加几类电梯除了要在ElevatorState中增加几类外,唯一可能大改的就是分配器的分配策略。如果要考虑电梯的性能的话,还需要考虑电梯与电梯间的换乘,因为我的换乘策略是打表得出第一次最优换乘,然后递归换乘,和楼层的总数也有关,不像一些dalao的是寻找最短路等,所以如果拓展更多电梯的话,换乘策略可能得推翻重写,所以我感觉就架构本身而言,可拓展性还行,但如果要提高电梯性能的话,就要大改换乘策略了。
2. bug分析
第5次作业
在互测时出现了一个玄学bug,我的设计是当为Morning模式的时候,让电梯等待4s再开始运人,但互测的数据显示我的Night模式和Random模式也等待了4s,而Morning模式等待了8s。
这个问题我最后反思了一下,可能是没有在电梯线程中等待,而是让读入线程等待了4s(但按理来说也只有Morning模式会让输入线程等待4s,其他模式应该也不影响的)
第6次作业
因为只在第5次作业的基础上增加了一个分配请求的分配器,所以似乎没遇到什么bug。不过强测rtle一个点,那个测试数据是从160s才开始进人,但我重交了一次又好了,可能是因为多线程并发造成的分配结果不同,所以有的电梯会跑的慢,当然还有一种可能是出现了死锁,不过我没有找出来,第7次作业也没有受这个影响(所以我觉得应该是第一种可能吧)
第7次作业
-
wait()
和notifyAll()
的问题,因为把WaitingRoom设置为分配器线程,所以增加了一个缓冲区类WaitingToAdd,导致在读入结束时忘记notifyAll()
,所以输入结束后电梯线程不结束。 -
我最开始的分配器不是线程,当读入到null后立即传个各个电梯,当各个电梯运完自己候乘表中的人后就会结束。但这样有一个很大的问题是,如果还有换乘的人,那么他从一个电梯A出来准备换到下一个电梯B的时候,B电梯可能已经结束线程了,这样就导致没有把人送到目的地。解决方法是将分配器改为线程,同时记录还没有换乘完的人数,等所有的人都换乘完后再将所有电梯的isReadEnd置为true>。
3. hack策略
因为多线程的复杂性和运行次序的不确定性,而且我也没有评测机,只能肉眼hack,所以我放弃了互测,那么说一下我自己自测时的测试策略吧。
首先对单个模块进行功能测试,再集成测试。测试用例既要有普遍的,也要有专门针对优化算法设计的,数量要足够多。尤其要注意测试线程能否正常结束,可以使用print的方式打印出run()康康现在进行到哪一步
4. 心得体会
4.1 关于层次化设计:
写这一单元最大的体会是提前打好草稿,想清楚自己要设计哪些类,每个类的功能都各自是什么,减少类与类之间的耦合(来自第一单元作业2次重构的血泪教训),每个类实现的功能要单一。
举个栗子:
-
主线程和读入线程分开,读入线程只负责读入和把请求放到缓冲区,主线程负责对类实例化和启动各个线程
-
我将电梯的自身运行(例如开关门、到达、进出乘客)这些事都扔给了
Elevator
线程自己去做,即Elevator
只管电梯的运行输出和进出人。把决定线程是否开关门、是否进出乘客、改变运行方向等这些事都丢给了Scheduler
去做,即Scheduler
管电梯自身的调度算法。
4.2 关于线程安全
4.2.1 关于wait()和notifyAll()
我感觉这一部分线程安全首先要自己默默在纸上分析,什么时候该wait()、什么时候该notifyAll(),当出现进程没有正常结束的时候(比如我第7次作业的bug),使用最朴素的print方法比调试更直观、也更容易找到是哪块出现了问题(我感觉,不一定对)。
多线程很多问题都是没有notifyAll
导致一直在等
或者是没有wait
,导致一直在轮询(多线程真是难啊)
4.2.2 关于Synchronize