- 前言
- 综述
- 设计策略
- 总述
- 第一次作业
- 生产者
- 数据托盘 DataCenter
- 消费者——电梯
- 第二次作业
- 生产者
- 托盘 ListenerToController
- 中央控制器 Controller
- 托盘 ControlleToElevator
- 电梯
- 第三次作业
- 生产者
- 托盘 ListenerToController
- 中央控制器 Controller
- 第三次作业的扩展性
- 程序分析
- 第一次作业
- 结构
- 复杂度
- 第二次作业
- 结构
- 复杂度
- 第三次作业
- 结构
- 复杂度
- 第一次作业
- BUG
- 第一次作业
- 第二次作业
- 第三次作业
- BUG Again
- 心得体会
前言
来北航上学,会让你变得惧怕坐飞机;
来北航学了OO半学期,会让你变得惧怕坐电梯。
乘坐那么多次电梯还活蹦乱跳的我,对那些写电梯程序对人心怀感激……
综述
第二单元据说是保留项目了——模拟目标选层电梯的运行,对多线程的程序设计进行了入门级的训练。从第一次作业的理想电梯——单电梯不限人数,到第二次作业的更有真实感的多电梯限制人数,最终却终结在了因为考核需要而进行了愚蠢的楼层限制、电梯分种类运行的第三次作业。
"时至今日,我仍然不明白,从2层上到3层,为什么不能爬"
相比第一次作业对于作业难度发展的失败预判,由于本单元作业更加现实的设定,从第一次作业开始便考虑将来的我终于实现了迭代(如果算的话)。把一个对象设计得更加“包容”诚然是件美事,这让我三周以来平稳度过(测评机搭建另说)。
设计策略
总述
总体来说,我三次作业使用的结构一致,应该算是贯彻使用了“生产者——消费者”模型。特别是第一次作业,基本是一个标准的模型,后续二三次作业则是两对生产者消费者的嵌套。
P.S. 要说自己电梯的调度策略是啥的话……其实我也没想懂大概就是ASL策略了——设置主请求并进行捎带。设计之初我觉得不是这样的……现在回头看看自己写的就是那么回事……
第一次作业
本次作业是单电梯,分析下来真就是标准的一个生产者——命令行输入客户——请求面对一个消费者——电梯。生产者消费者各有自己线程,除了托盘内数据其他数据并不共享。
生产者
生产者的话,课程组已经发了示例代码了,掉下来的代码没理由不用,欣然接受。在发的使用输入接口的示例代码加入初始化其他线程和最后结束其他线程的代码,就成我的生产者。
数据托盘 DataCenter
托盘基础的工作只有两个:被生产者加入新数据和被消费者取走数据。这里我让我的托盘多做了一些事情——把生产者传入的请求按请求的楼层进行了分类。不过由于自身并不是设计成线程模块,所以这件事情从线程看其实是生产者干的。
至于同步的话,所有public方法都加上了同步锁,说透了就是加入和提取互斥。
消费者——电梯
电梯设计为了单一线程,电梯自己判断自己的下一步动作,逻辑上优先完成电梯内的请求,电梯内空时会从数据托盘提取新的请求;每到一层也会从托盘查取是否有可捎带请求。
第二次作业
就这样,我们拥有了更多的电梯和更多的楼层,真是幸福呢……
就这样,我设计出来了中央调度器。对于命令行的输入来说,中央调度器是个消费者,而对于电梯来说,中央调度器是个生产者。中央调度器有自己的线程,它会对新进来的请求进行分派。
生产者
同上
托盘 ListenerToController
存放生产者与中央控制器的共享数据。因而加入同步锁进行同步控制。
中央控制器 Controller
单一线程,读入新请求、读取电梯状态并对请求进行分派。
托盘 ControlleToElevator
每个电梯对应一个此托盘。除了实现了第一次作业中的数据托盘的功能,其还外加了存放电梯目前状态的功能。电梯每次变更运行状态,例如到达新楼层,会对其状态进行更新,中央调度器会读取电梯状态,并以此来分派新的请求给最合适的电梯。本托盘中所有数据基本都为中央控制器和电梯的共享数据,全部public方法加入了同步锁。
电梯
第一次作业功能外,加入更新状态用代码。
第三次作业
“时至今日……” 不我不是复读机。
其实本次作业最大的变动,是一个请求可能会被拆分成多个请求去完成——即换乘。再者便是不同电梯的运行方式不同。诚然,要应对这两个变化,最应作出改动的是上述的“中央控制器”。
"但这么多的事情怎么好意思让控制器一个人全承担呢……"
找借口的话我一定会这么说,但实际上是我懒……
总之,分派请求给合适的、能够完成任务的工作,我教给了中央控制器,而另一部分工作我扔给了上述中的“ListenerToController”,或者说是生产者。
那么,除了前述各类各功能,新增的有:
生产者
本次生产者实际从命令行的输入变成了一个人。
这么说挺绕的……之前一个人只会产生一个请求,这次的话一个人会产生多个请求。人会在自己首次到达电梯或走出电梯需要进行换乘时向ListenerToController加入请求。
托盘 ListenerToController
把不能由一个电梯完成的请求拆分成多个请求,并将首请求放入等待队列,换乘请求放入换乘队列。
中央控制器 Controller
判断一个电梯是否有能力携带一个请求。
第三次作业的扩展性
整体架构的扩展性不错,各个线程之间都是通过托盘连接的,因此每个单独模块都可以实现“热插拔”,即基本符合“里氏替换原则”;单一责任原则考虑的话,个人感觉每个模块干的事情都是自己该干的,甚至还有第三次作业把本来应该中央控制器干的事情分给了生产者去干。
依赖倒置原则执行的也不错,但是最后第三次作业一个人的第一次请求和后续换乘请求公用了一个接口,这一点还需改进。
开放封闭原则执行的就不是很好,例如每次作业里面,电梯的可运行楼层数量是固定的,因为自己用数组存放楼层数据,而数组大小是直接规定好的。另外,第三次作业本应通过继承实现对电梯种类的划分,但等我意识过来就已经把原来的电梯类给改了……
至于接口隔离原则,第一次作业时为了后续作业多开放了一些接口,但后续作业全用上了,因此还算符合吧。
程序分析
果然和第一单元的数学计算相比,这次作业程序的复杂度有了大幅下降。
第一次作业
结构
复杂度
放眼望去,只有一抹红。那是遍历去查找下一个最合适的请求作为主请求的方法,如果想性能好一点的话,果然这个方法复杂度会上升。
第二次作业
结构
整体其实前文已经描述过了。个人觉得这样划分类是真正的每个类干自己的事情。
特别的,我还定义了两个宏定义用的类,分别用来映射楼层与编号、电梯与编号,方便数组的遍历。
同时把PersonRequest封装进了PersonInfo,并增加了进入电梯与走出电完成输出用的方法,使得出入电梯的行为从原来的电梯“吞吐”了一个人变成了一个人“上下”了电梯。
定义了HeatMap类,正如其名字,其记录者电梯的使用热点图,即已经分派给电梯的请求全部考虑后,每相邻两层间电梯需要携带的人数。
还定义了Compatibility类,用于记录每个请求与每一部电梯的“相性”,并重写了compareTo()方法,方便选择最合适的电梯。
复杂度
啊……红色又变多了。但是那些红色的方法,都是中央控制器在分派请求给最佳电梯时调用的。如果无脑分派的话,可定不会红了。在分派时会对电梯的状态进行遍历查询……除了遍历我确实想不到更好的方法(在不减电梯性能的前提下)。
第三次作业
结构
Comparibility类进行了部分更改,实现了判断某个电梯能否服务某个请求的判断。
PersonInfo变成了链表的形式,用于解决换乘的问题。
复杂度
"祖国山河一片红"了……
除了作业2中已经出现的复杂度问题,这次由于换乘的出现,对一个初始请求的拆分也加入了战斗。前文已述,自己对请求的拆分放在了第一个托盘,而新近变红的方法正是出自那里……
平心而论,我真的不知道怎么再把拆分请求这件事写的更简单……无论如何它都是一个深度优先或者广度优先的遍历吧。
BUG
写之前,作为多线程真正的萌新,被讨论区各种多线程bug难改的说法吓个半死。
写之后,发现自己上锁完全按照“生产者——消费者”逻辑,锁控制的很成功,反而发现bug全是单线程内部出来的……
我觉得我大概是个异类了……
不过bug只会迟到,不会不来
第一次作业
第一次是真正的构造电梯,是我三次作业中书写代码量最大的一次,后面两次都是在其基础上扩充。因此三次测试这部分可能存在的bug大概是极其变态的数据才能测出来了——不过这是后话了。
- 天真地认为电梯运行只有两种状态
开始真是“初生牛犊不怕虎”,直接给电梯的运行状态设计成了两种——上行和下行。就这样遗忘了“暂停运行”这个孩子,结果人家发飙了。
我的电梯只能向上运行——因为初始化认为电梯上行,申请请求时候也只找上行的请求。
就这样,我制造出来了“太空电梯”,这个电梯上升层数把整型搞越界了。
不过把电梯设计成三种状态已经是第二次作业了。第一次作业我让电梯在发现上行没有请求时候会转变状态为下行,暂时解决了这个问题。 - 找请求时候真的只找离得最近的
这其实不算bug了,不过跟自己设计初衷不一样,姑且也算bug吧。
开始时候我的电梯永远只服务离得最近的请求,譬如有3个请求分别在13、14、15层并且全是下行,电梯会傻乎乎地先接13层的人,再来14、最后15.
究其原因是从托盘中提取新的请求的问题。更改提取请求的算法以后解决了。 - 轮询
萌新不懂轮询什么意思就开始写作业了……于是我成功地写出来了轮询的程序。
初始时候电梯在未成功从托盘取出新请求时会每间隔一段时间(sleep()
)重新请求,而非运用wait()
并等待notifyAll()
。
比较意外的是似乎我电梯每次请求间隔时间足够长,上测评机竟然没有CTLE。
不过思来想去我还是改了。还是不要让电梯自己上闹铃起床了,被别人叫起来挺好的,“醒得早不如醒得巧”。
第二次作业
有了第一次的经验,再加上其实多线程的部分还是沿用了第一次作业,没出现什么大bug。值得一提的是,在互测中我被成功hack一次,那个数据的相同类型数据我在本地还测试过……更可怕的是修复bug时候我什么都没改就修复成功了……我还前后交了好几遍,都过了……
直到改第三次作业的bug,我才意识到了一点,想来可能就是这一点造成了上述情况:
- 数组越界
如前,我创建了HeatMap用于记录电梯的热点(见前解释),并用固定数组存放数据,但是在更新数据时候对边界没有分析清楚,导致在极少数情况下会导致数组越界。
更新了边界分析后很容易避免了越界发生。
第三次作业
一句话总结第三次作业的bug:
都是迭代的锅
- 楼层总数设置出错
相比第二次作业,第三次增加了16-20层。
如前,自己程序中按楼层存储信息时,都是按固定数组来建立的固定属性。
虽然更改了大部分的与楼层数量相关的代码,但总归是遗忘了一些。
在ControllerToElevator这个托盘中,电梯申请提取新请求时,其调用的方法会按楼层对其内部数据进行遍历,而我忘了更改楼层数量,因此每次都只会遍历15层一下的数据部分……
至于怎么改……恕我掠过……
BUG Again
这次是找别人的bug。
这单元作业不写自动测评机,连自己的bug都测不出来——人手完成不了定时输入吧。
所以也就设计自动测评机了。
- 生成输入
这次生成输入很简单,除了格式用的字符,其他都是一些整数或浮点数,也限制了数据范围,随机数即可。
当然,要想“变态”的话果然还是要自己生产数据。但这单元作业其实没啥真正 的极端数据可说,而且很多bug是要配上输入时间来实现的。所以全随机生成进行覆盖轰炸是最好的选择。
当然输入还是要考虑数据输入时间是,模拟高峰期一堆一起还是低峰时段独来独往。 - 调用程序
用python很容易实现指导书上的输入方法,感觉上其用了系统自带的函数和建立管道——不过也不用理解那么透彻,能用就好。
调用程序选择的是使用其jar包,主要是为了方便,后来多方学习意识到调用jar包是有其必要性的。 - 测试输出
这时,真正的悖论出现了……
有bug了,是电梯程序出来的,还是自己的评测机呢?“永远不要相信自己的程序没有bug”
不过最终还是在前两次作业使用了自动测评机检查输出。主要是检查输出数据,有没有吃人、有没有瞬移之类。RTLE马马虎虎限制了一下210s,CTLE没有管。 - 测试情况
总共测出来了一次RE,也没有去仔细读对方代码,但其有一个线程报错了,不是多线程的错误。
但交到线上没有hack成功……同类数据房友倒是成功hack了。 - 与第一单元差异
本单元数据点没有绝对意义上的“变态”,即极端数据点。每套程序都会在自己独特的地方出现错误。想要一个数据点hack多个人还是很难的。多线程的bug果然还是碰运气一样……
心得体会
- 接口可以不用,但不能没有
我真切地意识到了开固定数组来记录楼层是个多么愚蠢的事情。不然第三次作业强测分大概能翻3倍……
包括电梯也是,记录自己最大人数和ID的属性最好一开始就存在,虽然后来再加并不困难还可以用继承来解决,但开始就有果然还是最棒的。 - 里氏替换原则很好
耦合度低真的可以让你对程序其中一部分为所欲为。 - 该相信自己线程安全一定要相信
改第三次作业的bug前自己一直认为是多线程的bug搞得自己很慌……改完以后死的心都有了……“不要以为多线程了,单线程的bug就不存在了。”