BUAAOO Unit2:电梯作业总结

Unit2 电梯作业总结

0. 目录 

1. 作业分析

  1.1 第5次作业——单部多线程电梯

    1.1.1 需求分析

    1.1.2 主要思路

    1.1.3 主要的类及功能

    1.1.4 调度器设计

      调度器和线程的交互

    1.1.5 同步块的设置和锁的选择

      锁与同步块中处理语句直接的关系

  1.2 第6次作业——多部同型号电梯的运行,可以动态增加电梯

    1.2.1 需求分析

    1.2.2 主要思路

    1.2.3 新增类及功能

    1.2.4 调度器设计

      调度器和线程的交互

    1.2.5 同步块的设置和锁的选择

      锁与同步块中处理语句直接的关系

  1.3 第7次作业——多部不同型号电梯的运行,可以动态增加电梯

    1.3.1 需求分析

    1.3.2 主要思路

    1.3.3 新增类及功能

    1.3.4 调度器设计

      分配策略

      调度策略

      调度器和线程的交互

    1.3.5 同步块的设置和锁的选择

      锁与同步块中处理语句直接的关系

    1.3.6 架构的可拓展性

      UML类图

      Sequence Diagram

      功能设计和性能设计的Trade off

2. bug分析

  第5次作业

  第6次作业

  第7次作业

3. hack策略

4. 心得体会

  4.1 关于层次化设计:

  4.2 关于线程安全

    4.2.1 关于wait()和notifyAll()

    4.2.2 关于Synchronize

 

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:根据当前电梯的ElevatorStateWaitingRoom的情况,决定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:拥有所有电梯的ElevatorStateWaitingList,根据各个电梯的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 新增类及功能

  1. MyPersonRequest:与课程组提供的PersonRequest相同,增加了transferFloor,为换乘楼层,默认为到达楼层;增加了一个flag标志这个人是不是第一次进入分配器。

  2. WaitingToAdd:作为InputHandler输入线程和分配器线程之间的缓冲区,同时接收来自各个电梯的

  3. Elevator:增加了一个是否往WaitingToAdd回写乘客的判断,电梯在出人时需要判断这个人的换乘楼层和目标楼层是否一致。如果相同,那么说明这个人已经到达目的地;如果不相同,那么这个人会再次被放入WaitingToAdd中,等待分配。

  4. 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次作业一样,此处不再赘述。

第三个是缓冲区WaitingToAddWaitingToAdd是存放等待分配的候乘人,所以对它的读写取都需要加锁

锁与同步块中处理语句直接的关系

与第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次作业

  1. wait()notifyAll()的问题,因为把WaitingRoom设置为分配器线程,所以增加了一个缓冲区类WaitingToAdd,导致在读入结束时忘记notifyAll(),所以输入结束后电梯线程不结束。

  2. 我最开始的分配器不是线程,当读入到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

锁只加在需要加的地方,不乱加锁,只对共享数据进行加锁,保证数据的一致性。

 

 

posted @ 2021-04-23 08:26  哆哆啦  阅读(189)  评论(1编辑  收藏  举报