面向对象课程第二单元总结

多线程设计

hw5

参考了课上的设计,同样是InputThread线程读入输入,Scheduler调度器分配电梯的进出序列,ProcessingQueue作为电梯线程类执行电梯的运行、开关门、出入人;

加锁的共享对象包括输入等待序列waitQueue和电梯Process,其中Process存储了电梯自己的等待序列,为后面多部电梯做准备。

没搞明白加锁的原理,一整块代码都加锁了,相当于单线程程序……

hw6

架构上和hw5相似,为输入设置了独立的等待序列,Scheduler每次循环开始拷贝等待序列,相当于同步块里有三种等待序列,分别是输入的、调度器的、电梯各自的。

共享变量有四个,输入序列、公共等待序列、电梯等待序列、电梯链表,逻辑比较复杂,具体关系见下图:

image-20210422193338893

这次的设计可以说是非常冗余,正确性都难以保证。

hw7

彻彻底底重构了,改为自由竞争+LOOK算法

去掉了调度器,将Scheduler、ProcessingQueue、Process三个类去掉换成一个Elevator类

加锁的共享对象只有一个输入的等待序列inputQueue

同步块:

InputThread类中每次elevatorInput.nextRequest()都会进入同步块,对inputQueue进行修改

Elevator类中有三个同步块:

①循环头部判断输入是否结束、inputQueue是否为空;

②到达某层时,如果没有人要出门,查询inputQueue查看该层是否有能稍带的乘客进入,决定是否开门,更新inputQueue;

③开门0.4s关门前,查询inputQueue使得所有能稍带的乘客进入,更新inputQueue

LOOK算法:

当电梯内没人时,查询inputQueue更新进入序列,若进入序列内有同方向请求则维持原方向运行,若进入序列为空则电梯暂时停止,否则电梯反向运行。

可扩展性

hw5

是按照多部电梯的ArrayList只有一个元素设计的,是如果不是超时了的话,到hw6应该只需要添加elevatorRequest(然而

image-20210423143349739

线程间协作关系和课上一样:

image-20210423144904040

hw6

在第一次作业的基础上修改,又增加了一个公用等待序列

image-20210423145225442

线程间协作关系上,InputThread和Scheduler的交互减少(每次只要复制inputQueue就好了)

image-20210423145618452

hw7

UML类图如下:

image-20210422200333090

可以看出,由于不需要Scheduler给每个电梯调度分配,总体架构相比前两次作业简单许多

线程之间的协作关系:

image-20210422203242731

程序bug

hw5

“Passenger xxx already in elevator 1 so he/she cannot get in”

原来是采取了当到达targetFloor时才删除对应的等待序列(waitingQueue),下一次再清空电梯序列(inRequests&outRequests)的方式,于是下面这句话就成了锅了十几个点的罪魁祸首:

// class Scheduler
if (step == 1 && !processingQueue.isEmpty()) { 
    processingQueue.clear(); //清空电梯序列
}

我忘记了赶到主请求的fromFloor的过程中(step=1),也会有捎带请求,直接clear就会导致某个人刚刚被加入电梯序列并进入电梯,就会被清空,于是进入电梯两次。

将上面那段代码注释掉后,不得不更改清空等待序列和电梯序列的机制:

// class Process
//进人
processingQueue.getInRequests().remove(nowFloor);
//出人
processingQueue.subPersonnum(); //电梯人数减少
processingQueue.getOutRequests().remove(nowFloor);

以及更改删除等待序列对应请求的机制:

// class ProscessingQueue
ArrayList<PersonRequest> requests = new ArrayList<>(1);
 //request是捎带请求,加入电梯序列,从watingQueue中删除
requests.add(request);
waitQueue.getRequests().removeAll(requests);

连带着判断结束也得加一个电梯序列为空(因为还没到底targetFloor程序结束的时候watingQueue就空了)

// class Scheduler
//加了一个isEmpty判断电梯序列是否为空
if (waitQueue.isEnd() && waitQueue.noWaiting() && isEmpty()) 
//很悲剧的这一行超长度限制了,但是run方法恰好60行,不得不又写了一个方法
private boolean isEmpty() {
    for (ProcessingQueue processingQueue : processingQueues) {
        synchronized (processingQueue) {
            if (!processingQueue.isEmpty()) {
                return false;
            }
        }
    }
    return true;
}

“Passenger 134 cannot enter elevator 1 when the elevator is full”

应该在加入前就判断是否超载,而不是循环末尾。

image-20210422110729327

hw6

中测的时候,每次提交会随机(包括弱测和中测)有一个点RTLE,输入很短并且结束很早,看起来像是死锁程序没有结束,但是本地不可复现,强测中也没有再出现这个问题,一直找不到原因。

“REAL_TIME_LIMIT_EXCEED”

强测第8、9个点,输入分别在150~180s和集中在180s,时间较晚,调度算法写得性能太差,不能一次全部送走,导致超过210s的real time limit限制。

解决办法:优化了半天试图提高性能,调度器总是分配不够好,重构改成自由竞争了,即由 读入立刻分配给某些电梯的等待序列 变为每个电梯关门前检查是否有能捎带的人。

hw7

“REAL_TIME_LIMIT_EXCEED”

中测debug的时候遇到的问题。

如果输入结束后,每个电梯送完分配的人就进程结束,可能会导致去双层的人被B类电梯送到单层,而没有A/C类电梯送接下来的一层,导致程序无法结束。

解决办法:在所有电梯的共享对象中增加一个int stopnum = 0,当输入结束、等待序列中没有人、电梯没有人要进出时stopnum++,当stopnum==elevatorNum时唤醒所有电梯,结束进程。

心得体会

艰难debug的一个单元,三次作业重构两次,甚至于bug修复环节的超时问题死活不能解决,把下一次作业的代码交了上去(危,要被助教打死了

image-20210423143349739

由于第一次写的时候没想明白加锁的意义,以及加了一些自以为“可拓展”方便改成多部电梯的设计,导致第一次的代码性能和风格都很差。

第二次作业小重构,改的更加复杂了,Scheduler每次循环开始的时候复制InputThread的等待序列,产生了四种共享变量(输入序列、公共等待序列、电梯等待序列、电梯链表),逻辑过于复杂,非常容易出错。而且读入立刻分配给电梯的方式对调度方案的要求比较高,遇到输入全部在180s的样例就RTLE了,尝试了很久还是不能一趟全部送走,不得不重构。

重构改成自由竞争方式后,由于没有调度类,不需要考虑乘客如何分配给每个电梯,逻辑上简单很多,因此第三次作业比较顺利。

总体上,这一单元由于第一次作业没想明白加锁的意义和各种模式的优劣就贸然下笔,导致后面逻辑太混乱改起来非常困难,第三次作业积重难返不得不删掉所有类彻底重构。后面需要写代码前多看看不同方法的原理和优劣,注意架构的连贯性和可拓展性。

posted @ 2021-04-23 14:59  Beatrix_Y  阅读(83)  评论(1编辑  收藏  举报