301
社区成员
发帖
与我相关
我的任务
分享我们要模拟的电梯系统是一个类似北京航空航天大学新主楼的电梯系统,楼座内有多部电梯,电梯可以在楼座内1-11层之间运行。系统从标准输入中读入乘客请求信息(起点层,终点楼层),请求调度器会根据此时电梯运行情况(电梯所在楼层,运行方向等)将乘客请求合理分配给某部电梯,然后被分配请求的电梯会经过上下行,开关门,乘客进入/离开电梯等动作将乘客从起点层运送到终点层。请求的输入通过我们提供的输入接口来定时投放请求,你需要调用我们提供的接口,接口具体细节参考第三部分和相关文档。
可以采用任何电梯运行策略,即任意时刻,系统选择上下行动,是否在某层开关门都可以自定义,只要保证在电梯系统运行时间不超过题目要求时间上限的前提下将所有的乘客送至目的地即可。
一、功能描述
完成乘客请求,即对于每个乘客请求(起点层,终点层),需要调度电梯将其完成,并将必要的运行信息通过输出接口进行输出。具体而言,你需要控制电梯上下行,开关门的动作以及控制乘客进出电梯来将乘客从起点层运送到终点层。我们的电梯系统有多部电梯,你需要合理的调度这些电梯来达到更好的性能(第一次作业指定了接送乘客的电梯)。
二、输入输出格式
1.输入格式
每个乘客由指定的电梯从起点层接送到终点层。格式为:[时间戳]乘客ID-FROM-起点层-TO-终点层-BY-电梯ID。
2.输出格式
通过调用输出接口的每一次输出将自动附加时间戳于头部,具体请参考输出接口文档,请不要试图伪造时间戳,否则会在实际评测时产生不一样的结果导致程序运行错误。
电梯到达某一位置:[时间戳]ARRIVE-所在层-电梯ID
电梯开始开门:[时间戳]OPEN-所在层-电梯ID
电梯完成关门:[时间戳]CLOSE-所在层-电梯ID
乘客进入电梯:[时间戳]IN-乘客ID-所在层-电梯ID
乘客离开电梯:[时间戳]OUT-乘客ID-所在层-电梯ID
三、正确性说明
电梯运行基本约束
电梯系统默认初始在1层有6部电梯,电梯默认性能参数如下:
可到达楼层:1-11层
初始位置:1层
数量:6部
编号:6部电梯,ID分别为1-6
移动一层花费的时间:0.4s
开门花费的时间:0.2s
关门花费的时间:0.2s
限乘人数:6人
电梯在两层楼之间移动应满足移动时间要求,且电梯只能在大楼的楼层范围内移动,电梯移动时必须关门。
电梯默认初始时为关门状态,电梯只有开门才可以关门,只有关门才可以开门,且程序结束时所有电梯必须处于关门状态。
乘客默认不在电梯里,只有进入电梯才可以出电梯,只有不在电梯里才可以进电梯,且乘客只能进入同层的电梯。
乘客能且只能在电梯开门和关门的窗口期内进出电梯,但乘客进出电梯不需要时间(纸片人没有厚度)。
运行时的任意时刻需满足轿厢容量限制以及输出时间戳非减限制,系统结束时不可以有乘客请求未完成或乘客困在电梯里的情况出现。(此规定作用于所有作业的任何功能模块,对任何输出有效,以后不再赘述!)
本次作业必须由指定的电梯接送乘客,完成请求!!!即乘客只能进入和离开输入时指定的电梯,不可以进入其他电梯。

整体采用生产者消费者模型,除主线程Main之外有两类并发线程:InputThread与Elevator(其中Elevator由于有六台可以看作六个同类的并发线程),Main中初始化六个请求列表,分别由六台电梯“拥有”并各自独立处理,而InputThread则拥有一个装有总共六个列表的容器,根据输入将用户需求Guest装入对应指定电梯的容器中供六个消费者(电梯)各自处理
public static void main(String[] args) {
...
HashMap<Integer, RequestList> requestLists = new HashMap<>();
for (int i = 1; i <= 6; i++) {
requestLists.put(i, new RequestList());
} //初始化六个请求池
new InputThread(requestLists).start();
for (int i = 1; i <= 6; i++) {
new Elevator(i, requestLists.get(i)).start();
}
}
InputThread为输入线程,负责“生产”——从输入中读入用户请求,并将其放入请求池,结构及关键代码如下

public void run() {
ElevatorInput elevatorInput = new ElevatorInput(System.in);
while (true) {
PersonRequest request = elevatorInput.nextPersonRequest();
// when request == null
// it means there are no more lines in stdin
if (request == null) {
for (int i = 1; i <= 6; i++) {
requestLists.get(i).setEnd(true); //告知各请求池及电梯请求输入已经结束
}
break;
} else {
...
Guest person = new Guest(personId, fromFloor, aimFloor);
requestLists.get(elevatorId).addRequest(fromFloor, person);
}
}
...
}
Elevator类则扮演“消费者”角色,负责处理请求的同时将处理结果格式化输出

设计中参考了往届学长的架构,将电梯的上升与下降作为一个状态量ifUp存储在电梯自身的属性中,run()为电梯运行的主方法,其他几个方法功能如命名一样,负责实现电梯的基本行为。
这里要着重介绍的属性有requestList,dealList与analyzer。requestList即为main中初始化所建立的请求池,InputTread处理后但还没有被电梯响应的请求也就储存在这之中,即电梯门外等待的乘客请求储存池,而dealList则是与requestList结构相同功能不同的对象,负责存储已经进入电梯的乘客请求。
RequestList整体结构如下

fromMap是根据楼层存储请求的hashmap,如果是未处理请求池就是按出发楼层索引,使用addRequest添加请求,表示乘客已在对应楼层电梯外等待;如果是电梯中的已接受请求队列则是按目的地楼层索引,使用addGuest添加请求,表示乘客已经进入电梯。两种类型共用删除方法delRequest,效果是取出传入参数对应楼层的请求,将其返回并在原map中删除,表示已经处理完(或已从电梯外进入电梯内)。
endFlag用于标志输入是否结束,决策时需要用isEnd取得值用于决定是结束进程还是等待。
而analyzer则为电梯的调度策略类,此处内聚于电梯类内部是为了方便决策的处理,由电梯即时传入当前情况,决策器根据LOOK策略分析后给出决策结果,电梯再根据决策结果做出对应反应。其中决策结果用枚举类Strategy表示,简单直观。
//决策结果枚举类
public enum Strategy {
MOVE,WAIT,OPEN,END,REVERSE;
}

analyzer类结构(makeDecision即为核心决策函数)
Guest表示单个乘客的请求,包含乘客id等请求的所有需要属性,较为简单此处不过多赘述

下面来简单介绍在本次作业中使用的调度算法(LOOK)——
首先为电梯规定一个初始方向,然后电梯开始沿着该方向运动。
到达某楼层时,
首先判断是否需要开门
如果发现电梯里有人可以出电梯(到达目的地),则开门让乘客出去;
如果发现该楼层中有人想上电梯,并且目的地方向和电梯方向相同,则开门让这个乘客进入。
接下来,进一步判断电梯里是否有人
。如果电梯里还有人,则沿着当前方向移动到下一层。否则,检查请求队列中是否还有请求(目前其他楼层是否有乘客想要进电梯)——
如果请求队列不为空,且某请求的发出地是电梯"前方"的某楼层,则电梯继续沿着原来的方向运动。
如果请求队列不为空,且所有请求的发出地都在电梯"后方"的楼层上,或者是在该楼层有请求但是这个请求的目的地在电梯后方(因为电梯不会开门接反方向的请求),则电梯掉头并进入"判断是否需要开门"的步骤(循环实现)。
如果请求队列为空,且输入线程没有结束(即没有输入文件结束符),则电梯停在该楼层等待请求输入(wait)。
实现完之后的直观感受就是逻辑上比较简单,实现起来也清晰明了,也难怪被广泛采用。
此处对共享对象的请求池RequestList中所涉及的所有修改属性的方法都用synchronized进行锁定保证多线程并发的线程安全。

可以看到Analyze类之中的方法复杂度比较高,尤其是hasReqToDeal这个方法,因为是判断前方还有无请求的方法,涉及到的状态量和if-else判断比较多,也算是预料之中
总体来说本次作业难度并不高,难度主要来源于对思路上多线程并发的架构以及初次接触多线程的不熟悉,以及一些具体细节上的实现,思路理清之后按照多线程的规则和语法细心实现就问题不大。
再指明由哪部电梯来响应(两层调度?)
增加RESET指令,会改变满载人数和移动一层的时间
增加RECEIVE输出,拒绝自由竞争策略

第二次作业类图
新增调度器类Devider与“大托盘”类UnreceivedList,“大托盘”类用于储存输入但还未分配的请求以及分配完但是被reset指令抛回的请求,调度器类用于将“大托盘”中的请求按照调度策略分配到每个电梯所属的小托盘中。

由于需要随时获取六个电梯的信息,故将装有电梯的容器作为属性拥有,同时需要接入“大托盘”,findElevator与judgeElevator都用于寻找最适电梯(不过由于cpu开销过大原因这次干脆用随机数解决了)

第一次作业并没有实现,所以本次加入后一定程度上进行了重构,花了不少力气,undealRequests即为为分配请求的容器,inputEndFlag用于标志输入已经结束,endMap是一个存储六个电梯状态的容器,key是电梯id,对应value若为true表示当前电梯已分配的任务已全部完成,elevatorEnd在6部电梯均完成已分配任务后置true,用于判断线程结束的时刻。
相比较第一次作业而言增加了UnreceivedList这个共享对象,因此需对UnreceivedList类内部的方法用synchronized修饰,保证线程安全

本次复杂度主要集中于Devider类中,由于run中循环调用find与judge方法进行查询,导致复杂度过高,某些情况下cpu开销超过了要求,故直接废弃采用随机数了(
本次作业的难点主要在于结束条件的判断,因为不能和上次一样输入结束列表为空代表线程就可以结束,即使输入已经结束,UnreceivedList为空,之后可能也会有reset抛回的请求,对电梯也是同理,因此必须要输入结束,所有托盘均为空才能结束,解决方法上面已经陈述,不再赘述。
两种重置请求:
第一类重置请求仅修改电梯参数,重置参数请求包含需要重置的电梯ID和电梯相关参数(满载人数、移动时间)。程序需要在重置完成后让电梯以新的参数运行,重置完成后,电梯处于原楼层(同上一次作业)。
第二类重置请求将电梯修改为双轿厢电梯,重置参数包含需要重置的电梯ID,换乘楼层,两个轿厢的相关参数(移动一层的时间和满载人数)相同。当重置完成后,轿厢 A 默认初始在换乘楼层的下面一层,轿厢B默认初始换乘楼层的上面一层。(本次新增)
两种重置操作的区别仅在于结果(重置操作完成后的影响),完成过程及与RECEIVE的约束关系与上次作业完全相同。
双轿厢电梯是指在同一电梯井道内同时拥有两个独立的电梯轿厢,而电梯系统默认的普通电梯是指在一个电梯井道内只有一个轿厢。为了保证两个轿厢不相互碰撞,将楼层分为上区、下区、换乘楼层,其中上区为换乘楼层以上的所有楼层,下区为换乘楼层以下的楼层,均不包含换乘楼层。在整个运行过程中,要求轿厢 A 只能在下区和换乘楼层运行,轿厢 B 只能在上区和换乘楼层运行,同一井道内的两轿厢不能同时位于换乘楼层。请思考双轿厢电梯的优势(包括双轿厢电梯耗电量优势,具体定义见性能分说明部分),合理设计调度方案。
整体还是沿用上次的调度策略,这次没有新增类,对于第二类reset请求将原本的电梯一分为二重新压进电梯容器视为一台新电梯即可,同时需要在共享的表中增添共享楼层的占用属性,用于防止同一井中的电梯相撞。
本次作业得分很不理想,debug花费很久最终效果还是不尽人意,bug集中出现在CTLE轮询与无法正常结束的RTLE,归根到底是之前采用的几个类多个线程过度耦合的判断结束策略不太恰当,算是第二次作业的遗留问题。应该还是新建一个共享类用来计数已经输入请求数与已经处理完成的请求数,当两者相等且输入已经结束时统一结束。
同时本次的迭代策略也算不上优秀,让本来可拓展性不高的第二次作业雪上加霜。最终会得到这样的结果也是迭代思路上的一次失误吧
第三次作业给我的直观感悟就是在史山上迭代有多痛苦,再次深刻体会到低耦合高内聚的设计原则在面向对象程序设计中的重要性,第三次作业之所以会寸步难行,就是因为第二次作业以及迭代思路上多个类之间属性方法耦合程度过高,互相调用太频繁(共享大池子,小池子,电梯,调度器)基本上每动一个其他都要受影响,这样的设计对迭代是很不好的。希望在之后的作业中能够切实改正这一陋习,提高自身的面向对象编程素养。