BUAAOO-第二单元总结-电梯调度

面向对象第二单元总结与反思

0 题目概述与博客说明

第二单元作业的目的是模拟电梯的运行过程,在三种模式(Morning、Night、Random)下实现对客人的接送,其中输入输出模块已由课程组实现。三次作业层层递进,由单部电梯到三部同型号电梯再到三部不同型号的电梯。

0.1 第五次作业

本次作业的基本目标是模拟单部多线程电梯的运行。

思路与解题历程

本次作业较为简单,我采用的是Producer——Customer模式,生产者是电梯输入线程(InputThread),用于读入乘客请求;消费者是电梯线程(Elevator),接受并处理乘客请求。乘客状态表(PersonTable)用于存储当前各楼层乘客的等待状态。
此外,为了实现程序良好的拓展性,我增设了调度器线程(Scheduler):输入线程将乘客请求放入WaitQueue中;调度器从WaitQueue中读取请求,并将其放入乘客状态表(PersonTable)中;电梯内置处理器基于某种算法选择相应的顺序从乘客状态表中读取处理乘客请求,并控制电梯运行。

电梯调度策略——Process类

本次作业我设置了三种运行方法,分别用于处理Morning、Night和Random三种模式。三种运行模式均采用ALS捎带算法;事实上,仅Random一种就可以处理所有情况,且无明显时间差距,Morning和Night单独处理换来的性能提升空间有限

程序Bug分析

自身Bug

为了在不失速度的情况下尽可能减少电梯运行次数,我在Morning中选择对请求等待1.2秒,尽管大多数情况下都是没问题的,但对于较多分散请求,且目标楼层均较低时,我的程序会出现多次等待,由于第一次作业对电梯运行时间的要求较为严格,因此该方法超时了3个点,将等待删除后就修复了。

互测Bug

本次测试中,与我同组的同学出现的bug主要是:
1、无法正常停机,出现死锁
2、有位同学的Night模式调度错误,会一次性接完所有的人,这样会出现超载现象。

0.2 第六次作业

本次作业要求模拟多部同型号电梯的运行,并要求能够响应输入数据的请求,动态增加电梯。

思路与解题历程

第六次作业的主要难点是如何避免两部电梯对同一个楼层响应,造成请求冲突和时间冗余。在写第六次作业之前,我就已经有了两个思路——集中式调度和分布式调度。
集中式调度需要对每一个楼层设置脏位(dirty bit),用于记录该楼层是否有电梯响应,同时修改电梯调度策略(Process类),使其只响应未设置脏位的楼层,并且将响应楼层的dirty bit置0。
分布式调度则是修改调度器(Scheduler类),将不同的请求分给不同的电梯,这样尽管会出现电梯运行功耗的增加,效率也略有下降,但因其可拓展性良好,且修改的代码量较小,我虽然也写了集中式调度的版本,但最终提交时仍然选择了分布式调度。
此外,需要注意的是,调度器的分配算法也会对电梯运行时间产生影响。我一开始尝试了基于局部性原理的分配算法,即满足尽可能使请求分给与上次请求差异最小的电梯中与尽可能分给请求数较少的电梯中。但后续结果表明,基于最小请求数优先的分配算法在通常情况下具有较好的运行速度,与其他分配算法差异不大。局部性原理的分配算法仅仅保证了电梯运行次数较少,达到了降低功耗的作用,具有一定的现实意义

程序Bug分析

自身Bug

本次我的bug均来自Random的处理算法,一开始,我考虑了请求的公平性,即在处理请求时尽可能优先处理较早到达的请求,这种调度尽管大多数情况下没有问题,但对于极端数据会出现较大的波动,hack我的那位同学正是找到了这一点。

互测Bug

本次互测我没有找到其他人的bug。

0.3 第七次作业

本次作业要求模拟多部不同型号电梯的运行。型号不同,指的是开关门速度,移动速度,限载人数,可停靠楼层的不同。
image

思路与解题历程

我在第5次作业中设计的电梯架构保证了电梯的移动速度和限载人数可以在初始化时直接确定(将在3中进行介绍),因此本次的主要任务是处理电梯停靠楼层的不同
有了第6次作业的经验后,本次作业我的思路是修改调度器(Scheduler类),只要将合适的请求分给不同型号的电梯,那么该电梯就只能在合适的楼层停靠。关于拆分请求的操作,我将在可拓展性章节进行详细的介绍。
此外,对于不同型号电梯的处理,我的方法是利用工厂模式,在创建电梯时即赋予电梯参数,同时,电梯均使用相同的处理器。

程序Bug分析

自身bug

与死锁不同,本次作业我的bug主要是因为捎带策略产生的等待锁。由于在捎带时未对拆分请求进行区分,导致我的电梯频繁响应一个“虚拟”的请求,最后产生死循环。

他人bug

本次作业我的互测屋中的同学或多或少均存在程序无法正常退出的现象,经过对他们的代码进行分析,可以看到绝大多数同学没有正确处理好同步机制,造成程序出现死锁。

1 同步块与锁

本单元作业均采用生产者——消费者的设计模式,共享数据对象有waitQueue和personTable,因此在进行同步块与锁的设置时,仅需要对上述两个对象进行分析,实现对RAW、WAW、WAR三种数据冲突的处理。

以向personTable中增加元素为例,首先对共享对象进行上锁,接着向其中增加请求,最后调用notifyAll(),唤醒正在等待的线程:

synchronized (personTable) {
	personTable.addRequest(request);
	personTable.notifyAll();
}

此外,为避免线程轮询,可在等待队列为空时调用wait(),尝试让当前线程等待,如:

if (!(waitQueue.noWaiting() && waitQueue.isEnd())) {
    try {
        personTable.wait();
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}

2 调度器设计总结

调度器是电梯运行的灵魂,调度器的好坏决定了电梯运行的好坏(为方便讨论,本节调度器是Scheduler和Process的统称)。从本人的设计经验与和其他人的讨论中来看,调度器的设计大体有两个趋势:集中式和分布式。通常情况下,采用集中式的效率会比分布式的效率稍高一些,但开发难度较大。

集中式调度

集中式调度是基于一个多电梯共享的personTable对象,Scheduler仅仅将请求加入personTable中,不对请求进行处理。
集中式调度面临的问题有:如何避免不同电梯对同一个请求响应,此时将会产生数据冲突,且会造成效率降低。第6次作业中提到了一种处理方法是对每一层楼设置上行——下行脏位,从而在一定程度上解决了这一问题。
此外,在和@王卓浩同学的讨论中,如果取消对电梯设置主任务,而是采用自由竞争的算法,或是其他不依赖主人物的算法,如look算法,可以较好的解决这一问题。

分布式调度

分布式调度事实上同时为每个电梯维护了1个乘客请求表(personTable),Scheduler则实现动态地将请求分配给合适的电梯,实现较为简单。另外,经本人不完全的测试,采用最简单的分配算法(基于最少请求的分配算法)与其他分配算法在时间上并无太大差别。
分布式算法的缺点是运行时间具有较大的波动性,对于不同数据,其运行时间可能存在较大的差别;此外,采用分布式的电梯往往会出现冗余处理现象,在功耗上要显著弱于集中式调度,因此现实中的应用较弱。

3 第三次作业的可拓展性

本单元作业中,我将电梯的状态抽象为ElevatorState,并在其中设置了许多复用性良好的方法,如:addRequest(int floor)、rmRequest(int floor),实现了对floor楼层所有请求的响应;另外,对于电梯的状态,我增加了int型变量up_down(取值1:上行;取值-1:下行),用于表明电梯是上行还是下行,从而将电梯的移动抽象为floor+up_down,实现了上楼与下楼的一体性。
此外,关于拆分请求的操作,我将拆分后的请求重新放入waitQueue中等待Scheduler后续处理,同时将拆分后的请求尾端插入splitQueue中,电梯响应请求当且仅当该请求ID不位于splitQueue中;同时,电梯每处理完一个请求就从splitQueue中删除第一个相同ID的请求。关于拆分的算法,不同的人有不同的思路,在此不进行赘述。

UML类图

image

UML顺序图

image

可拓展性分析

首先,本程序实现了ElevatorState与Process的独立,同时将其封装在Elevator中,这样做的好处是电梯仅仅是一个抽象的外皮,如果更换不同型号的电梯,只需要修改ElevatorState;如果需要更换电梯服务算法,只需要更换Process。此外,代码的可复用性极强,如果需要更改题目,如增加负楼层,或是需要解决其他实际问题,如火车运行调度,仅仅需要更换类名,将楼层改为车站,而不必对方法进行大规模的改动。
其次,程序将“调度(Scheduler)”与“处理(Process)”分开,即使需要增加电梯,也不必进行大规模的改动,只需要更改调度策略,将部分请求分配给新加入的电梯,以第6次作业为例,仅仅在第五次作业的基础上增加了一个15行的方法,就完成了作业。
最后,由于广泛使用设计模式,代码具有较强的模块化,各个类之间的依赖关系不强,具有良好的可拓展性。

4 BUG寻找策略与评测机思路分享

BUG寻找策略

1、多线程最重要的是避免死锁的产生,首要测试点应是产生大量高并发的数据进行多次评测;
2、其次,对于无捎带策略,最明显的缺点是会产生超时现象,而第6次、第7次作业对于互测输出Tfinal的要求均在70s,因此可以采用特殊边界数据,比如:

[1.0]Random
[1.0]2-FROM-2-TO-1
[53.2]3-FROM-4-TO-1
……
[53.2]51-FROM-20-TO-1

此数据利用了电梯的极限运行状态,在空闲时期直接到达20层等待数据输入,之后逐层接人,计算电梯的开关门时间和移动速度,恰好在70s时运行结束。
3、第7次作业同时要注意请求拆分过程中是否有电梯在非法楼层开关门,我的评测机只迭代到了正确处理请求的版本,因此本阶段的数据处理方式是利用word搜索乘客ID,手动监测乘客的进出情况。

评测机构造

与第一单元相比,本单元的评测机数据构造十分简单,困难的地方是如何实现定时投喂数据。我利用了python的subprocess,同时写了输入解析函数,利用管道将输入重定向到待测程序中;输出正确性的检查则需要构造状态机。

5 心得体会

本单元作业给我的压力要远远小于第一单元的作业,一方面是因为我在完成第一单元的作业时对Java的各项操作仍不甚熟悉;另一方面是因为本单元我的第一次作业的架构可拓展性很强,后续版本仅进行了较少的迭代。
第一单元的博客作业中我曾写道,“自动化的测试工具可以减轻90%以上的debug负担,同时可以尽情hack别人”,这次我吸取了上次的经验,事先搭好了一个较为简易的评测机,在后续作业完成中体会颇深。
在本单元结束时,我关于多线程的理解得到了进一步的加深。线程安全在本单元的体现较弱,在保证对请求状态表与waitQueue的互斥读写后,就不会再出现线程安全问题了。知识最重要的是学以致用,本单元我在多次迭代后利用python成功搭建了一个多线程的评测机,较好的练习了多线程的知识,并成功hack了很多人(逃ε=ε=ε=┏(゜ロ゜;)┛

6 补充内容

关于电梯的改进方法,我的一些不成熟的建议是:
1、增加对公平性的考量,即避免出现让最先到达的请求持续等待的情况,评测时可设置waitTime,记录每个请求的等待时长之和,用于评估该项指标,作为性能的一部分;
2、增加对电梯功耗的考虑,即对于多部电梯,避免出现在n部电梯可以在相同时间完成任务的情况下有n+1部电梯响应请求的情况,评测时可设置位移s,记录每部电梯运行楼层数之和,用于评估该项指标,作为性能的一部分;
3、实际情况下,电梯的移动速度并非线性变化,而是具有加速和减速的过程,以笔者所居住公寓电梯举例,体现在从1楼到6楼所需时长仅仅是1楼到2楼的2.6倍左右,并非本单元作业的6倍。未来作业中可增加电梯的加速与减速过程,作为性能评估的一部分;
4、由于多线程的程序具有不确定性,在一次测试中很难找到所有bug,即使当前的测试点通过,但如果进行多次测试,仍然可能出现问题,因此希望增加评测机对数据进行多次测试的功能。

(另外,关于完整的改进思路我做了一个文档,有兴趣的可以点此处下载:https://files.cnblogs.com/files/blogs/675407/关于电梯改进的一些思路.zip

posted @ 2021-04-26 23:56  Stanlei  阅读(123)  评论(0编辑  收藏  举报
// 侧边栏目录 // https://blog-static.cnblogs.com/files/douzujun/marvin.nav.my1502.css // // // // // //