301
社区成员
发帖
与我相关
我的任务
分享总体来说第二单元的背景是“基于一个类似北京航空航天大学新主楼的大楼,电梯可以在楼座内 1−11 层之间运行”,我们会"时序地"收到一系列请求(包含如1-FROM-2-TO-10这样的乘坐请求和RESET-ELEVATOR-2-3-0.5这样的电梯设置请求)。
我们需要用线程的方式运行每一个电梯,将请求分配给电梯来完成。
不难看出,我们从外部获得输入的请求,将请求分配给电梯,这符合生产者-消费者模型,因此一个遵循生产者-消费者设计模式的、管理Request的容器类Requests就尤为必要。
在此之外,我们还需要关注进程的结束,外部输入结束之后,我们需要依次通知输入类、调度类和电梯类结束线程,总而结束我们的总进程。
6个电梯,运行在1-11层,请求形如"乘客ID-FROM-起点层-TO-终点层-BY-电梯ID",也就是说我们的调度器只需要查看请求要求的电梯ID并分配给指定电梯即可。
设计的难点:电梯的运行时序;容器类的设计。
乘坐请求不再指定电梯。新增RESET请求,形如"RESET-Elevator-电梯ID-满载人数-移动一层的时间",接收到reset之后要尽快停靠,放出乘客并调整自身参数。
迭代可能带来的难点:调度器结合电梯参数与运行情况进行分配;被reset的电梯会弹出乘坐请求;外部输入结束之后,调度器仍可能收到reset电梯弹出的请求,不能立刻停止运行。
新增双轿厢reset,形如“RESET-DCElevator-电梯ID-换乘楼层-每个轿厢的满载人数-每个轿厢移动一层的时间",重置后两个电梯分别在换乘楼层上下,两个电梯中,A的可达楼层为[1,换乘楼层],B在[换乘楼层,11],且AB不能同时来到换乘楼层。
迭代可能带来的难点:调度器需要面对的情况更加复杂;电梯接收到的请求可能不能一次完成;双轿厢重置会销毁原电梯线程,启动两个新的电梯进程。
首先以下介绍一下在三次作业都通用的同步控制:容器Requests类、数据流向。然后我们会介绍数据流向在三次作业中的变化
由于Requests类是课上实验给出的类,此处省略代码,直接给出Requests的时序关系:

消费者(输入类)不断调用getOneReq方法,获取到null之后结束处理。
getOneReq之后,首先判断requests是否为空,不为空则取走一个request返回,否则:判断是否end(end意味着requests是否可能有新的请求到来),end则返回null,否则:等待....等待再次被唤醒有两种可能:1.新的请求到来,此时取走新的请求;2.setEnd,此时返回null。
我们用一个形象的例子类比一下这个情况,我作为消费者去面包店购买面包,这个店每天都会制作面包,等到面粉用完之后会关门。当我发现面包店开门且有面包时,我会直接买一个面包;当我发现面包店开门但是没有面包的时候,我会等一会儿,可能过段时间新的面包制作好了,我便买面包离开,也可能过段时间面包店发现面粉用完关门了,我便空手而归,知道买不到面包了;如果我来到店里发现店已经关了,那么我自然也就直接回去。
很显然实验提供的Requests类也并不是对所有生产者-消费者设计模式都通用,我们用面包店为例,是否有可能我还有别的事,如果发现面包店没有面包,我会留下电话请求面包店有新面包之后打电话通知我,而我先去忙别的事情,而不是傻乎乎在原地等着(因为我必须有面包才能做接下来的事情)?是否有可能有的人一次性需要多个面包?是否有可能有多种原料,多种面包?总之生产者-消费者模型多种多样,本单元的实验给出的容器类只适应:一种面包,乘客一次买一个,消费者愿意等待面包(因为没有面包他们不能做后续的事情)...

我们以hw5的伪类图展示一下设计架构中request的流动关系,InputHandler直接对接输入,作为unallocatedRequests的生产者;调度器Allocator从unallocatedRequests中取出待分配请求,经过一定的调度算法后,分配给某个电梯(具体为放入某个电梯的requestList中),也就是说Allocator是unallocatedRequests的消费者与各个电梯的requestList的生产者;
电梯内部的流动关系大致为:requestList->toIn->passengers->toOut。电梯内部的流动关系见"决策scheduleDirection & scheduleDoor"与"执行上下行 & 开关门"
在电梯的架构中,我们将每一次循环划分为两个阶段:决策与执行。决策的时候可以对requestList上synchronized锁,但是不可以sleep;执行的时候不可以对requestList上synchronized锁,可以sleep。这样我们确保了电梯类对和调度器共享的容器类进行上锁的时候不会sleep,因而电梯不会长时间阻塞调度器影响效率。

这个过程是决策过程,不会有sleep的操作,可以对requestList加synchronized锁。
依据当前状态决定下一步的移动:Idle(空闲,不移动)、Up、Dn(Down),并判断有哪些乘客将要进来(从requestList移动到toIn),哪些乘客将要出去(从passengers到toOut)。

在这一步,开关门,让一些乘客进入(从toIn到passengers),让一些乘客离开(从toOut删除),然后移动。
当外部输入结束之后,我们需要传递End状态(End状态对于一个容器类意味不会再有新的输入),很显然,End状态的传递是:unallocatedRequests->requestList->toIn->passengers->toOut,输入线程在获取到null请求后结束,调度器在unallocatedRequests结束且为空后结束;电梯线程在toOut结束且为空后结束。
最核心的变化在于,加入reset和doubleCarReset后,外部输入的结束不代表没有新的请求进入unallocatedRequests了,因为reset可能弹出请求,A、B双轿厢电梯可能接取自己不能独立送达的请求,尽力送达后会弹出未完成的请求。
对于第二次作业,进行以下迭代:

如图中紫色所示: InputHandler收到ResetRequest之后会通知Allocator,Allocator会赋值对应电梯的unhandledReset。电梯在下一次决策时,会检查unhandledReset,如果有未执行的reset,会进入reset状态;电梯完成reset之后,在向Allocator弹出未完成的request的同时,会通知Allocator。如此,Allocator会记录分发下去的reset请求数量已经完成并弹出了的reset数量,当二者数量不等时,表明有电梯还未执行完reset请求,此时不能对unallocatedRequests设定End。
对于第三次作业,进行以下迭代:

DoubleReset会新增两个电梯线程,并销毁原来的电梯线程。其中双轿厢在move移动时可能访问共享对象,在"双轿厢"中介绍这个部分。
采用循环调度策略,也就是依次分发给每一个电梯,如果将要分配给的电梯在reset的过程中,会等待。
在第三次作业的时,对于双轿厢电梯,当且仅当电梯可以让乘客更接近目标楼层时才会分配给电梯:
例如,1-FROM-5-TO-9,如果A电梯的可达楼层为[1,4],则A不可以接收这个请求;如果电梯A的可达范围为[1,6],会将请求封装为1-FROM-5-TO-6交给A电梯。A电梯到达目标楼层后会检查,发现请求未完成,将会弹出给调度器。
双轿厢电梯有一个共享对象floor,在将要移动时,如果试图进入共享楼层,会调用floor的synchronized tryIn()方法,如果楼层正在被占用,将等待。当电梯离开共享楼层时,会调用floor的synchronized out()方法,解除占用状态。同时,电梯如果将要在共享楼层停靠等待,会强制其离开共享楼层。
public synchronized void tryIn() throws InterruptedException
{
if (Main.isLog()) {
Main.getLogger().info("try in, occupied: " + occupied);
}
if (occupied)
{
while (occupied)
{
wait();
}
}
occupied = true;
}
public synchronized void out()
{
if (Main.isLog()) {
Main.getLogger().info("try out, occupied: " + occupied);
}
occupied = false;
notifyAll();
}
目前作业遇到的bug主要都是调度引起的,核心都在于电梯没有Buffer缓冲。
1.第二次作业中,循环调度遇到电梯reset就会放弃分配给这个电梯,转而尝试分配给下一个电梯,这会造成:当所有电梯都reset时,调度器会循环占用CPU资源,导致CTLE;当只有一个电梯没有reset时,会将所有任务分配给这个电梯,造成RTLE。
解决方法:当将要分配的电梯reset时,调度器进行等待,保证任务相对平均分配给各个电梯。
2.第三次作业中,doubleReset的电梯会销毁,因此调度器如果试图分配给一个正在doubleReset的电梯,会先进行等待,然后被唤醒后发现是doubleReset,进程已销毁,于是分配给下一个电梯。当五个电梯被doubleReset只剩下一个电梯的时候,仍然会将所有任务分配给一个电梯。
解决方法:当调度器分配给doubleReset的电梯时,被唤醒后发现原电梯已销毁,按照一定的策略分配给其分裂出的A、B电梯。
多线程debug的时候,断点往往力有未逮,有的时候直接运行会有错误,断点导致线程的中断反而没有错误了,因此这种时候,有效地通过对外输出来记录很重要。
我采用日志的方式记录关键信息,初始化:
logger = Logger.getLogger(Main.class.getName());
logger.addHandler(fileHandler);
logger.setUseParentHandlers(false);
logger.info("info");
调用日志输出:
if (Main.isLog())
{
Main.getLogger().info("new req input: " + request + " to req: " +
toReq + " reqed: " + reqed);
}
日志截图:

感觉多线程,同样是synchronized,用一个专门的类的synchronized方法会比在代码块中synchronized清晰得多,我在作业的主要架构中便是以生产者-消费者的容器类Requests解决主要的多线程问题,用代码块的synchronized进行辅助,达成一些灵活的操作。
但是即便如此,也应该做到自己知道synchronized时是在做什么,譬如我第三次作业,在对requestList上锁时就是决策阶段,这个阶段决策涉及的变量,包括requestList、unhandledReset都不应该发生变化,同时这个阶段对与调度器共享的对象上锁,应该避免sleep方法,减少调度器的等待。只有对线程时序有清晰的把握,才能避免多线程错误的发生。
同时,日志是一个很好的debug方式,一方面不输出到控制台干扰评测,另一方面随关随开也更加方便,还可以自定义格式、等级。