OO第二单元博客
目录
架构
第一次作业
架构设计
-
架构中共有输入线程、调度器线程、电梯运行线程三个线程,采用双生产者-消费者模型。
-
第一个生产者-消费者模型:生产者为输入线程,消费者为调度器线程,托盘为总的请求队列。
-
第二个生产者-消费者模型:生产者为调度器线程,消费者为电梯运行线程,托盘为分电梯的请求队列。
调度策略
-
严格采用look算法,即在某一方向运行时,若当前层有请求,且请求的运行方向与当前相同,则开门接收乘客。若电梯内没有乘客,且前方没有新请求,则转向。
UML图
第二次作业
在第一次作业的基础上,新增了横向电梯和多部电梯。
迭代开发
-
设置了电梯父类,横向电梯和纵向电梯分别设置为其子类。
-
电梯运行线程的设计,本来也想设置父类,但在实现的过程遇到了一些阻力。最终放弃,转而新增了一种横向电梯运行线程,好处是实现比较简单实用,有着较大的灵活性,坏处是两种电梯运行线程的相似度确实比较高。
-
调度器的设置,将请求分配给横向电梯托盘或者纵向电梯托盘。
调度策略
-
通过调度器把请求分配到对应楼层或楼座的等候队列,该楼层或楼座的所有电梯运行线程共享此托盘,让所有电梯“自由竞争”,即控制器不对指令的分配有过多约束,而是发挥各电梯的自主能动性。
-
在每个电梯运行线程内部,依旧采用look调度策略。其中横向电梯由于基准look有可能卡到电梯一直“追赶”却不开门接人的bug,故将横向电梯的换向策略改为
电梯内部没人的情况且无同方向待上乘客时,该座若有反向乘客则接人并掉头
。 -
在和同学们的讨论中发现,大多数同学均采用自由竞争的策略。它的优点在于实现简单且在随机生成的数据时性能较好(基本保证乘客尽快进入相应电梯)。但缺点是比较浪费电梯资源,会出现多个电梯哄抢1人情况。
UML图
第三次作业
在第二次作业的基础上,新增了换乘请求。
迭代开发
-
采取静态分解请求的策略,整体架构并未作改动。
-
调度器新增逻辑,实现了换乘请求的电梯分配(具体见下)。
-
分配到纵向电梯的请求需要设置中转层替代第二次作业中的目标楼层,成为乘客下电梯的条件。笔者采用寻找距离5层最近的满足条件的横向电梯作为中转层,相比基准策略算是一个简单的优化。
调度策略
-
大体与第二次作业类似,采用自由竞争+look策略。
-
在迭代的过程中,先修改的横向电梯运行线程,出于求稳和便于理解的心态,我彻底舍弃了look策略,转而采用了同方向顺时针一直转的接人策略,仅在电梯里没人的情况下增加了一点特判。
UML图
由于第二,三次的架构几乎不变,故此处不再画出。
UML协作图
同步块与锁
由于三次作业设置同步锁的思路类似,且是迭代开发而来的,故此处仅分析第三次作业的同步锁设计策略。
锁方法
笔者对RequestQueue类和EleQueue类的全部方法设置了同步锁。其中RequestQueue类为所有请求的总托盘,被输入线程和调度器线程共享;EleQueue类存放由调度器分配到某个电梯的分托盘,被调度器线程和电梯运行线程共享。
-
RequestQueue类
public class RequestQueue {
public synchronized void addRequest(Request request);
public synchronized void removeRequest(Request request);
public synchronized Request getOneRequest();
public synchronized void setOver(boolean isOver);
public synchronized boolean isOver();
public synchronized boolean isEmpty();
}
-
EleQueue类
public class EleQueue {
public ArrayList<MyPersonRequest> getQueue();
public synchronized void add(MyPersonRequest request);
public synchronized void addAll(HashSet<MyPersonRequest> requests);
public synchronized void removeRequest(MyPersonRequest request);
public synchronized void removeAll(ArrayList<MyPersonRequest> requests);
public synchronized void removeAll(HashSet<MyPersonRequest> requests);
public synchronized boolean isOver();
public synchronized void setOver(boolean over);
public synchronized boolean isEmpty();
}
锁代码块
笔者的架构中,共有输入线程、调度器线程和电梯运行三个线程。故在这三个线程中,涉及到对于如上两个托盘的查看或修改时,需要将对应的托盘锁住。实现举例如下:
synchronized (requestQueue) {
if (requestQueue.isEmpty() && getOver() && remainRequest == 0) {
for (EleQueue eleQueue : elevatorsTQueue) {
eleQueue.setOver(true);
}
for (EleQueue eleQueue : elevatorsHQueue) {
eleQueue.setOver(true);
}
request.setOver(true);
return;
}
}
调度器设计
受到第一次实验课中助教的“暗示”,笔者修改了第一次作业的架构,加入了调度器线程。在第一次作业中,调度器的功能简单,仅用于将新加入的请求分配到对应楼层。
第二次作业中,由于新增了横向电梯和新增电梯请求,调度器的功能略微增加:如果是新增电梯请求,则添加电梯运行线程并初始化电梯;否则判断是横向电梯还是纵向电梯,将请求分配到对应的楼座或楼层,最后每个电梯再自由竞争获取对应楼层或楼座托盘中的请求。
由于第三次涉及到换乘的问题,调度器相比前两次显得不可或缺。相较第二次的调度器,新增了关于换乘请求的分配逻辑,描述如下:
-
如果请求的起始楼层和终止楼层相同(不需要换乘)则调用竖向电梯分配逻辑
-
否则,若存在横向电梯满足
起始楼层==电梯楼层&&电梯在起始楼座和终止楼座均可开关门
,则调用竖向电梯分配逻辑 -
若以上两点均不满足,则将请求分配给起始楼座对应的竖向电梯队列。
if (request instanceof PersonRequest) {
if (castRequest.getFromBuilding() == castRequest.getToBuilding()) {
dispatcherT(castRequest);
} else {
int flag = 0;
for (EleRunnerH eleRunnerH : eleRunnerHs) {
if (conditions) {
flag = 1;
dispatcherH(castRequest);
break;
}
}
if (flag == 0) {
dispatcherT(castRequest);
}
}
} else {
dispatcherE((ElevatorRequest) request);
}
测试与bug分析
三次作业中强测均未出现bug,第二次作业被盒友hack了一次(最后发现评测机有一个地方写错了导致bug没测出来,啊多么痛的领悟)。
bug分析
bug提示是,在第十层输入了两次arrive,经过一番排查最后发现,是因为我在电梯移动利用max(floor-1,1)和min(floor+1,10)
来保证电梯不会“上天入地,导致在本该输入“到达11层”的时候,输入了两遍“到达10层”。所以本质上,还是look算法中调转方向的问题。
原来的代码中,当电梯等待队列里没人的时候,调用look算法会进入default版本调转方向,而且电梯每运行一层都会调用三次look算法,导致电梯的方向在我没意识到的情况下,经常换向。在修改中我对于队列为空做出特判,同时添加代码保证电梯在10楼的方向一定向下,在一楼的方向一定向上,修好了这个bug。
测试
在三次作业中,笔者均使用了自动评测机检验代码的正确性,并通过和小伙伴对拍的方式排查了一下RTLE
的问题(顺便看看自己代码的性能有没有太拉垮)。评测机可以支持自测,多人互测,输出cpu时间以排查CTLE
的问题等。令人悲伤的是,第二次的校验逻辑有一处笔误导致互测出了一个bug,以及由于数据生成强度的问题,在互测中并未hack到人。
心得体会
线程安全
层次化设计
在本次作业中,我采用了策略和状态分离的层次化设计。将电梯的实际运行策略(look或者摩天轮)放在elevatorrunner类中,而将电梯状态放在elevator类中。运行策略类会根据电梯内的人的数量,队列中人的数量等信息,决定电梯移动的方向,并对状态进行修改等(如上人下人),这样的设计有利于方便为电梯状态设计不同的调度策略,降低了整个电梯运行控制逻辑的耦合性。