零、前言
期盼着,期盼着,电梯来了,死锁的脚步近了(大雾
本单元作业围绕 “多线程” 与 “架构设计” 展开,以 “电梯” 的设计为载体。在形式上,三次的实现并不算困难,但若想在已有设计上更上一层楼,在 “性能” 上进一步突破, 并仍然有着良好的 “线程安全” 性,则在设计方式的各个角度都要更进一步。
一、设计策略
1.1. 第一电梯与第二电梯
由于在第一次电梯就实现了LOOK的算法,在第二次电梯时就不用换架构了。也就是说我的前两次电梯架构是辩证统一的,故放在一起来讨论:
大体思路:
以楼层类为核心,调度器为发展基本点构建体系。
对于楼层类,其实现的定位于RequestQueue(请求队列)相同,都是一个关于请求的容器。不同的是,请求队列要求请求之间以时间顺序严格排列,队列本身的构造被这种排列方式约束。而楼层类根据请求的fromFloor不同分配到不同的楼层,进而还可以更据其他属性在楼层里进行再分类,这就引入了“请求分配”这个预处理的概念,为之后的扩展留下了充足的空间。
架构有两个线程,其一是输入线程,其二是电梯的运动线程。调度器首先定位是一个能获取全局信息的对象,并且他只在电梯需求有获取信息的时候刷新。因为其被动刷新和常处于阻塞状态的特性所以我这次并没有为其分配给其线程。
电梯成为了一个执行者,也是仿真事件的推动者,在每个楼层都会触发一次仿真事件并执行调度器传过来的“命令”。
此处附上相关代码,方便表明电梯和调度器二者的角色关系:
自定义的Enum类:
public enum Situation { upperFloorHaveReq, downerFloorHaveReq, haveReqNowFloor, allReqDealtAndExit, haveNoErNorFr, haveUpperErToDeal, haveDownerErToDeal }
Elevator相关源码:
Situation situation = sdr.dispatcher(elevCar, nowFloorNum); switch (situation) { //sometimes although exit many situation, dispatcher // preferentially return : //(0) exit / have noErNorFr //(2) the req in the recorded orientation //(3) Er relevant case haveUpperErToDeal: case upperFloorHaveReq: elevStatus.setUpForward(); if (theWorldEnable) { TheWorld(); } if (theWorldEnable) { TheWorld(); } break; case haveDownerErToDeal: case downerFloorHaveReq: elevStatus.setDownForward(); if (theWorldEnable) { TheWorld(); } if (theWorldEnable) { TheWorld(); } break; // in this situation, we could follow with init status: wait case haveReqNowFloor: elevStatus.setWaitStatus(); break; case allReqDealtAndExit: break RunningLoop; case haveNoErNorFr: Macro.sleepUntil(idleTime); continue RunningLoop; default: System.err.println("Unknown dispatch situation"); break; }
Scheduler相关源码:
public synchronized Situation dispatcher(ElevCar elevCar, int nowFloorNum) { //S = {all situation} refreshStatus(); if (allReqDealt) { return Situation.allReqDealtAndExit; } //S = {reader is work(ignore), have ElevReq, have FloorReq} //isWait <==> elevator has neither upper nor downer ElevReq, which // not mean it has not ElevReq at nowFloor // if it is also has no nowFloor ElevReq, it IDLE ///in another way, IDLE <==> have no ElevReq in the ElevCar //for ALS mode if (!elevCar.haveEr() && !haveFrReq()) { return Situation.haveNoErNorFr; } for (int j = 0; j < orientationNum; j++) { if (isElevGoUp) { if (elevCar.haveUpperReq(nowFloorNum)) { return Situation.haveUpperErToDeal; } for (int i = Macro.num2index(topFloorNum); i > Macro.num2index(nowFloorNum); i--) { if (flrs[i].haveReq()) { return Situation.upperFloorHaveReq; } } } else { if (elevCar.haveDownerReq(nowFloorNum)) { return Situation.haveDownerErToDeal; } for (int i = Macro.num2index(bottomFloorNum); i < Macro.num2index(nowFloorNum); i++) { if (flrs[i].haveReq()) { return Situation.downerFloorHaveReq; } } } //before reverse, check now Floor Req if (elevCar.haveReqToFloor(nowFloorNum) || flrs[Macro.num2index(nowFloorNum)].haveReq()) { return Situation.haveReqNowFloor; } //record reverse only if this orientation have no requests to deal //and at this moment we choose to reverse the orientation record elevReverseRecord(); } return Situation.haveNoErNorFr; }
对于多线程的保护比较方便,由于唯一会在不同线程之间共享的就是楼层。而楼层又是很经典的读写者模型。所以我只需要控制电梯和输入线程对于某一个楼层不会同时写,或者使用CopyOnWrite的队列就可以保证线程安全。
下四张为UML图,第一、二张是UML协作图(通信图),第三张是在包的角度,第四张则更加关注类本身的实现与细节。
1.2. 第三电梯
UML协作图:
这幅图更多地说明了分配器的作用,以及程序中对于PersonRequest(PrReq)信息的流动
Class和Package视角看架构:
第三个电梯架构和之前两次完全相同,代码基本达到了复用的目的。
唯一不同的是增加了分配器的类。分配器是分配算法的具现化。这个算法在我的优化博客里有细谈,
二、度量分析
2.1. 单电梯
真男人从不回避自己在MeticCalculation里那一抹鲜红(坚定)
本次电梯开始并不存在过分耦合的地方,但是为了优化,在一些区域需要有更大的视野,这就导致了对象之间的链接过分紧密,耦合性增强。
在另外一些优化处由于使用了复杂度较高的算法,连带着整体的耦合度分析也有些许病态。
具体而谈,飘红的地方一方面是我的对拍程序,处于我的架构之外,自不必多谈,其他的主要有两种原因:
(1)转向算法中贪心的描写需要增强Scheduler的 “视野”
(2)随手在电梯中实现的计算使用ALS而非LOOK算法会有怎样的执行顺序,会产生什么样的结果。这个简略电梯实现的比较随意,因此也造成了结构上的不美观。
2.2. 多电梯
真男人从不回避自己在MeticCalculation里那一抹鲜红(掩面哭泣)
多电梯和单电梯的毛病基本类似。
(1)重度使用的对象(分配器)和过分耦合的对象(调度器和分配器几乎有所有对象的引用)使得程序耦合性过强,出红的点也集中在这两个类上。
(2)在Scheduler里实现的getFloorTime()方法是用来模拟下一段时间电梯运行的实现,也就是在电梯里实现了一个电梯,是贪心算法的重要组成成分。由于第三次作业要考虑的因素很多,计算量很大,数值上表现的病态也是意料之中。
三、分析自己程序的bug
本次作业没有正确性bug,最后一次的互测有一个elevatorInput中出现的bug,通过层层剥析,最终定位到elevatorInput.close()方法,注释掉后输入程序不再抛出异常,问题也就不存在了。但除此之外本地无法重现bug,甚是玄学。
经验是我们应该对每一个异常使用一套健全的异常处理机制,包括try catch finally以及具体的实现,而不是简单catch完打印异常信息了事。
四、发现别人bug采取的策略
4.1. 正确性bug
“拍就完事了。”
对输出的逻辑诊断和定时投放系统共同构成了这次的评测姬系统,通过调整数据的产生密集程度(包括区域密集程度和时间密集程度),形成不同的情景,进而全方位扫描,进行攻破。
4.2. 线程安全bug发现策略
想起上课时老师对哲学家进餐的性能安全bug所做的分析:等待的时间越短,性能bug越容易复现。
我这次也是基于这样的原则,将测试程序的运动时间和开关门时间减小到原来的1/10,并且将投放时间间隔缩短到原来的1/10,然后进行对拍,线程安全的程序能正常运行,并且就算缩短到1/100甚至更短也不会有任何问题。
但是性能安全有问题的程序就会寸步难行了。
4.3. “收获”
除了第一次作业是在是打不出什么问题,第二次和第三次电梯作业互测环节都顺利Hack住了他人。可见即便是A屋,正确性也不是百分之百良好完密的,没有程序是绝对的标程。
此外在性能安全方面,第二三次互测环节我用了上文所述的方法去检查性能安全。结果并不尽人意,只有几个同学能完美活过线程安全的加速。但出于正常测试这种bug出现几率极小,因此并没有试图用这些数据来Hack住他人。
五、有关优化
优化部分值得细细讨论,故此另开一优化博客单独探讨优化
优化博客地址:https://www.cnblogs.com/Eaglebu/p/10754293.html
六、个人感想
这个单元的作业感觉比起上一单元更加得心应手。一方面是由于对JAVA语言语法和IDEA环境的熟悉,但更多的是所选的架构的优异性。
Floor型架构比起RequestQueue型架构而言处理有关先后顺序的需求稍显不足,但胜在预先分拣,减轻了调度器与电梯的计算压力。
良好的预先分析和充分地准备和设计往往能制作出来更为优秀的架构,而这些架构也能为后续的 需求扩展 或者 性能优化 奠定极好的基础。