OOBeiHang Unit2 Report
The Elevator!
前言
造电梯的过程,仿佛比电梯本身,更有趣。
自由竞争与规划调度之争,也正是令人心动的自由。
目录
一、调度设计
-
调度方法
-
换乘策略
-
调度器与其他线程的交互
-
同步块与锁
二、整体架构与拓展过程
-
三次作业UML类图与多线程分析
-
拓展过程
-
拓展心得
-
未来拓展能力
三、结构分析与代码分析
-
采用的设计模式
-
设计原则分析
-
度量分析
四、bug分析
五、反思总结
-
如果这次的代码让你没有拓展的欲望,果断重构吧!
-
拓展不能故步自封,拓展本就是件灵活的事呀!
-
设计止于完善
-
优化程度与正确率并不会相斥
-
变化性、戏剧性与众多有关无关的思考构成了本单元的时光
六、结语
一、调度设计
调度器包括调度线程Schedule、和三个共享对象waitLine、verticalLines、circleLine。所完成的任务是实现输入线程InputThread的乘客投入与电梯线程Elevator的乘客选择取出。
1. 调度方法:
三次作业中均采用的主方法是改进的look算法(以竖向电梯为例):
(1)相关参数与运行过程: 电梯运行描述参数为 运行方向direction,curFloor,desFloor。 每次运行循环过程为 判断开关门、移动(curFloor += direction)、更新目的楼层(update desFloor)、判断是否转向(update direction)。
(2)目的楼层的确定: desFloor = ( direction == 1? max(maxUp,maxDown) : min(minUp,minDown) ) 即上行时目的楼层取上行乘客最大到达楼层与下行乘客最大出发楼层中的较大值。 下行时目的楼层取上行乘客最小出发楼层与下行乘客最小到达楼层中的较小值。 每次到达后,会更新目的楼层。
(3)转向判断: 若运行到达目的楼层,则转向,同时更新目的楼层。
(4)等待判断: 若该楼座对应verticalLine(即待乘队列)为空且电梯内无人,则进入等待状态wait。
此外,在电梯的选择中使用自由竞争,不加干预调度。
2. 换乘策略:
本次中换乘策略通过分析尽可能将流程固定下来。我们也将乘客分为四种类型,同楼直达、同层直达、同层换乘、异楼换乘。
归纳他们的运行方式,可以发现只有两个换乘过程,即verticalLines to circleLines sent by buildingElevator、circleLines to verticalLines sent by floorElevator,也就是从竖向队列中进入楼座电梯运送后发送给横向队列,以及从横向队列中进入楼层电梯运送后发送给竖向队列。
3. 调度器与其他线程的交互:
第一次作业中,调度器与电梯的控制器交互,这种方法不太优良,原因在于两点。 其一是共享对象waitLine由三个输入、调度、电梯线程共享,且事实上,电梯还是多个进程,降低了性能且增大了风险; 其二是电梯线程与调度器线程通过共享对象电梯控制器交互,而电梯控制器又不能完全解决电梯的所有功能,这样造成了两难的困境,若是功能都通过控制器解决,就失去了电梯设置进程的意义;若大部分功能转移回电梯,则又导致共享对象超高频调用,不符合设计原则。 所以我在第一次作业之中采取了折中的措施,说是折中解决问题,实际上则同时承担了两个坏处。
在第二次作业中进行了重构,简化了线程间的交流。 调度线程与输入线程完全通过共享对象waitLine进行交流,采用生产者-消费者模式,即通过两个方法put、get完成全部功能。 调度线程与电梯线程完全通过封装后的verticalLines与circleLines交流,电梯接人时通过调用同步方法实现交流。第二次中,这种交流是单向的,即只有电梯分别从两个队列中接人,第三次作业中,这种交流变为了双向的,电梯还要向队列发送请求。
4. 同步块与锁
通过上文分析,线程间的交流完全通过共享对象的put类和get类方法,所以只需要将这些方法加synchronized即可。
我在三次作业中都没有遇到过任何死锁的状况,分析其原因,是因为死锁一般出现在两个线程都在抢两个共享对象的锁,而本架构中只有一种可能出现,即第三次作业中的verticalLine和circleLines,但电梯线程不会同时企图使用两个共享对象,且在共享对象变更状态时都会使用notifyall(),避免了死锁的出现。
事实上在电梯的架构中,同步块往往出现在状态的改变而非查询。只要锁住状态的变更,查询就不会出现在变更期,所以数据只有新旧之分,而时间先后一般不会影响到正确性,所以理论上只需要对引起队列状态变更的方法加synchronized。
二、整体架构与拓展过程
1、三次作业UML类图与多线程分析:
第一次作业的类图:
这里DesCur类是电梯的控制器,用于控制电梯的运行,作为调度器与电梯线程的共享对象inRun。
第一次作业多线程分析:
第二次作业的类图:
第三次作业的类图:
这里VerticalLines类与CircleLines类的出现是对ArrayList<Passengers> 的封装,为了能够更好的使用单例模式。
几乎所有的共享对象都为Passengers类或其衍生,便于同步设置。其中,verticalline[i] 是所属楼座为 i 的等待队列,通过增加共享对象类的实现,减少线程间的共享频率,同时由于是为同一类的实现,并没有增加工作量与额外的风险。
第二三次作业多线程分析:
注:这里一个verticalLine/circleLine只被该楼座的电梯共享,图片在这方面描述不贴切。
2、拓展过程:
第一次到第二次,由于对第一次架构的信任崩塌,果断采取重构。
第二次到第三次,拓展总共用时为:设计2小时,实现2小时,调试1小时。工作量几乎仅仅为半天多,可见良好的拓展性强的架构所带来的好处。出去拓展的部分主要分为三部分:
(1)类的信息拓展: 由于乘客开始需要换乘,所以当前阶段的目的地不会是整体的目的地。为了保证电梯机制不动,我们对乘客类做了改变。因为(fromFloor, toFloor, fromBuilding, toBuilding)四元组是电梯获取乘客信息的基本元素,所以我们以这个四元组作为乘客当前阶段的刻画,这样可以保证电梯机制仍能完美运行,此外我们增加了四个量:(from, mid, to, curBuilding),分别表示总出发楼层,中转楼层,总到达楼层,当前楼座。前面分析乘客路线可总结为三段式,纵-横-纵,所以这四个元素可以实现两种换乘过程。
(2)功能实现拓展: 为了完成任务,需要加入相应的功能,包括获取中转楼层的getMid()方法、发送请求的sendRequest()方法、乘客状态转移的setFromTo()方法。比较重要与困难的部分是确定终止状态与特殊状态,如如何判断乘客是否完成整个流程、以及mid = from时的特判。
(3)微小细节修改: 这一步最微小,但却最重要,也最容易出现bug。由于整体机制发生了改变,需要完整的分析哪些地方需要修改。
(4)架构增量开发: 在第三次作业中,为了更好的实现线程结束同时保证运行性能,我引入了整体计数器来对当前乘客数进行计数,当减为0时唤醒所有线程并执行setEnd()。
3. 拓展心得:
(1)拓展过程中,决定变哪个不变哪个极其重要,通过此次实验,我得到的经验是可以将原来的机制模块化为新机制下的一个关键模块,再通过增加其他模块或改变接口信息来实现拓展。此外,修改类信息要优先于修改运行机制。 (2)拓展也需要大量的设计,而且不同于普通设计的是 拓展设计过程中要分析多种拓展方式,而不是遇见问题解决问题的思路。同时,要在设计阶段完成对特殊状态与状态转移细节的设计分析,将之纳入设计出的整体状态系统;避免在实现过程中不断通过增加判断来单独处理新发现的特殊状态。
4. 未来拓展能力:
(1)性能优化:性能优化主要在两个方向,第一是延续现行算法,寻找最优中转楼层与最优同层到达;第二是计算所有路径代价函数取其优。 第一个现有算法较容易实现,只需要模仿现有两种换乘方式再加入CircleLines to CircleLines ,然后加强终止条件的判断,就可以实现多次换乘。如此,只需要在计算中转楼层时考虑同层换乘,以及在同层优先选取最快方式即可实现。
(2)功能拓展:功能性拓展大部分是改变或增加电梯、乘客的限制条件,可以从类的属性上下手,然后改变参数,新增相应方法。
三、结构分析与代码分析
1. 采用的设计模式
(1)单例模式: 单例模式优点是避免了同一个对象在多个线程中作为参数屡次调用,使用前提条件是该类只能有一个实例。其中,封装后的VerticalLines、CircleLines以及GlobalCounter都采用了单例模式。有两种常见的地方使用单例模式可以简化过程:1. 某实例作为多个类的内部变量。2. 某实例可当作全局变量使用。事实上,很多内部变量是以ArrayList或HashSet等实现,这样我们可以将其包装为一个类,即可以使用单例模式减少调用,也简化了结构的复杂性。
(2)工厂模式: 工厂模式的优点是将电梯的构造接口化,本次作业中对其体会不深。
2. 设计原则分析
(1)单一职责原则(Single Responsibility Principle) 第一次作业中电梯控制器的设计不符合单一职责原则,二三次中对等待队列的的包装使得职责更加单一明确,不足点是大量方法仍然在最基础的类中实现,没有细化到不同的职责类中。
(2)开闭原则(Open Closed Principle) 在第三次作业中良好的反映了开闭原则。整个电梯模块除了调用参数外没有做任何改动,主要拓展点在于本身发生变化的类,即乘客类。
3. 度量分析
第一次作业:
第二次作业:
第三次作业:
总体看来本次圈复杂度整体良好,少数几个关键模块复杂度较高。
三、bug分析
本次一共出现三个bug,可其中两个bug造成了巨大的杀伤力。
(1)第一个bug出现在第一次作业,产生原因是是wait的条件过于苛刻,导致大量轮询,在强测造成了大量的 CPU Time超时。
(2)第二个bug出现在第二次作业,产生原因是输出线程类的奇葩设计。由于我在写输出类时采用的类似于队列的写法,产生一个push进去,然后pull出来一个,导致如果前一个arrive由于锁的问题延迟而后一个arrive没有延迟会产生时间过短的问题。最后我将输出类只负责同步输出,解决了这个问题。
(3)第三个bug出现在第三次作业,产生原因略为迷惑。由于前面我屡次提到不改动电梯机制,结果由于我在拓展过程中过度秉承这个理念,导致横向电梯接人时忘记判断时候能下,导致乘客要去B座而电梯没法开门,就在焦急地走来走去。 由此看来,我们在拓展设计中的理念还是过于稚嫩,不能过分坚持。
在测试方面我并没有投入过多的精力,只进行了简单的功能性测试,前两次互测我基本没有出手hack也没有被hack,但第三次比较激烈,我利用换乘的特殊情况构造数据一次hack成功了五个人,但同时由于前面提到的重要问题被hack了8次。
四、反思总结
如果这次的代码让你没有拓展的欲望,果断重构吧!
这里回答了上个单元提出的问题:“何时应该选择重构?”。准确地说,如果这一次作业完成之后,脑子里对整体的机制还没有清醒的认识,或者对自己的运行逻辑没有十足的信心,甚至没有去拓展的想法与欲望,这就是选择重构的时机。
事实上,重构并不难,但是任务量重,相反,拓展往往具有更高的难度。一次好的设计,应该在看到拓展需求的时候,脑子里会有一丝想法,而不是仍然一片混沌。
拓展不能故步自封,拓展本就是件灵活的事呀!
本次作业在拓展的时候,也许是偷懒的心思在作祟,我有点偏执的坚持不修改电梯部分,最后在重要机制上犯了错误。可见,拓展本无章可循,唯一能遵循的,是完善的逻辑与脑中的模拟。一方面完成增量部分的加入,还要再脑中模拟运行,看是否在某些调用上要修改,是否某些条件要加强。
由于下次作业的规格性更强,希望能找到更好的严密的拓展方法。
设计止于完善
本单元尤其是二三次作业,我的设计几乎止步于完成整体的架构,让他能够以接口的形式模拟运行起来。比如线程应该调用什么函数完成什么工作。但事实上,设计止于完善也同样意味着设计没有止境。因为在开始实现之前永远不可能知道内部的困难。很多本来设计的很简单的方法实现时往往会拆成很多部分甚至会增加新的类。上个单元中我疑惑与设计应该到什么程度在开始实现,也许是过于割裂两者的关系。更合适的描述应该是:在设计中实现,在实现中设计。用实现的细化程度去设计,在实现中遇到不那么容易的地方,要用设计的思想去更好的实现。而不是设计时就整体画几笔,实现时就不动脑子硬实现。
在第三次作业中,我本来准备对同楼层换乘进行优化,优化完后在测试时遇到了一个bug,调了一会后就放弃了优化,担心会影响到自己的正确性。但是戏剧性的是,当时遇到的bug竟然不是由于优化造成的,也就是前面提到的第三个bug。由此可见,优化并不是一定会造成程序复杂,进而导致正确率下降,有时,在优化的过程中,还会发现之前的错误进而改正。
变化性、戏剧性与众多有关无关的思考构成了本单元的时光
我总是喜欢思考一些无关的东西,比如从电梯运行的自由竞争联想到自由市场与政府调控,并思考许久,企图建立起一些联系,也企图借助电梯对经济的调控有一些新的认识,这些也算是电梯带给我的收获吧!此外,电梯中出现的一系列让人哭笑不得的bug也是快乐与悲伤源泉之一。
五、 结语
“朋友,您开始脱头发了。您得当心点呀,您才不到三十岁,秃头对您太不好看,您把生活看得太严肃了。”
——安德烈 · 纪德