OO第二单元博客作业

一、作业分析

  本单元作为OO课程第二单元,我们主要通过多线程的手段实现了电梯调度,通过我们平时比较熟悉的电梯来训练多线程编程,并重点培养我们线程安全的意识。本单元的作业难度相较于第一单元有所提升,究其原因是同学们是第一次接触多线程编程比较陌生,会出现很多莫名其妙的线程bug。

1、Homework1

  本单元第一次作业是单部可稍带电梯,我听说上一届第一次作业写的电梯只是一部“傻瓜电梯”,因此一开始构思的时候有些烦躁,不过构思完之后发现其实捎带并不是一件很困难的事情。本次作业的结构设计采用的是一种生产者-消费者模式(当然后面两次作业也是一直沿用此模式),输入类Input(生产者)不断向调度器Contorller(托盘)中的请求队列里投入电梯请求,而电梯Elevator(消费者)发现有请求投入调度器里后开始工作。

1.1 程序结构分析

UML分析

OO第二单元博客作业_第1张图片
OO第二单元博客作业_第2张图片

复杂度分析

OO第二单元博客作业_第3张图片
OO第二单元博客作业_第4张图片

代码行数分析

  从上图的复杂度分析可知此次作业整体复杂度并不高,由于是第一次设计,绝大部分代码集中在elevator类中,并且elevator的run方法过于臃肿,完成了很多不应该是他干的活,复杂度过高,违背了SOLID的SRP原则,这个问题在之后的作业中有所改进。

  此次的调度策略采用的是指导书中的ALS策略,每当电梯到达一个楼层之后,电梯就会遍历一遍调度器中的请求队列(后来想想把所有的请求按照请求的起始楼层分好类,这样的话电梯每到达一层楼之后遍历该楼层的请求队列就好,这样子在电梯请求很多的情况下优化效果或许更加明显),如果当前电梯的主请求存在,请求的运行方向和当前电梯运行的方向相同的话就可以捎带。我在设计电梯调度策略的时候也考虑过其他常见的调度算法,比如Scan算法等,发现每种算法都有明显不如其他算法的情况,因此我认为大体上来讲这些算法的平均性能都是差不多的(不包括一些dalao手搓的算法),最后强测的结果还算可观。

1.2 bug分析

  本次作业我在强测和互测中都没有被发现bug,还算是不错的,一方面可能是因为第一次作业难度不是特别高,另一方面也和我评测之前疯狂找bug有关。

  在debug的过程中我遇到了很多问题,比如说,一开始我在elevator的run方法中读取请求队列的信息,如果为空,那么controller就需要wait等待,其实这么写会报错的,因为此时电梯根本没有用到controller的wait,wait就会出现问题,说白了这就是对于wait-notify机制和锁机制不理解造成的bug。然后我听从学长的建议,把取请求和存入请求的方法全都放到调度器里面去加上sycronized关键字,基本上就不会出现线程安全问题。

  我还发现的一个bug就是判断结束错误出现的电梯“吃人”问题,我在判断电梯线程结束的时候只判断了输入端是否结束和电梯当前主请求是否完成,忽略了捎带请求的目的楼层不在主请求目的楼层和起始楼层之间的这种情况,也就是说,当主请求完成之后,此时电梯内部还有一个捎带请求,导致该请求被关在电梯里无法出来。

  在互测中我并没有找到其他人的bug,首先是因为当时我只会一股脑将所有的请求输入到调度器内,并不会按照时间点依次输入请求,导致有一部分可能情况无法实现;另外可能是第一次作业不是特别困难,大家也都做了充分的测试。

2、Homework2

  本单元第二次作业为多部可稍带电梯,在第一次的基础上增加了可运行电梯的数量,同时限制了单部电梯最大载客数量,难度上相较于第一次作业提升并不多。

2.1 程序分析

UML分析

OO第二单元博客作业_第5张图片
OO第二单元博客作业_第6张图片

复杂度分析

OO第二单元博客作业_第7张图片
OO第二单元博客作业_第8张图片

代码行数分析

  本次作业的设计新增了ElevatorStatus类,用于将电梯的状态实时传递给调度器;由于本次需要通过输入端输入可运行电梯的运行数量,因此我将原本的输入类Input的功能直接放到了主函数MainClass里面,否则建立两个输入端(一个位于MainClass,一个位于Input内)会出现非同寻常的bug(该点在后续会进行描述),因此导致我的MainClass复杂度提升不少,另外由于多部电梯的出现,需要更细致的调度,Controller类内容明显增加,其中AddReqeust方法复杂度显著提升,电梯run()方法还是一样的臃肿(在第三次作业会改善)。

  此次作业最大的困难点在于如何处理多部电梯获得请求的问题。在我的设计中我采用调度器分派请求给各个电梯的策略,请求队列由原先的一个总请求队列变成数个电梯的局部请求队列和一个等待分派请求队列,当一个电梯请求到来后,调度器根据当前所有电梯状态选择适合的电梯,将请求加入到该电梯的局部请求队列中:首先找能够捎带该请求的电梯,如果不存在,就找离请求出发楼层最近的空闲电梯,如果还不存在,那么就将该请求加入到等待分配请求队列中去,等到有电梯的状态改变了,再判断处于等待分派请求队列中的请求是否可执行。这种方法直接将请求加入到一个电梯的局部请求队列中去,这样每个电梯就都是独立的,运行策略就是第一次作业中的电梯,这样做只需要修改AddRequest方法,增加等待分派队列相关方法就可以了。最后强测的性能得分也是比较满意的。

  第一次作业的调度器说实在的算不上是一个调度器,它只是用来存放请求队列的一个缓冲器一个托盘罢了,但是此次涉及到多部电梯,就产生了调度和分派问题了。因此需要电梯实时将自己的状态,比如当前运行任务是什么、电梯内有多少个请求、电梯内有多少人等等信息传递给调度器,调度器以此来判断该将请求分派给哪个电梯。

  由于电梯有了最大容量上限,那么捎带策略也有略微改动,即如果当前电梯内的请求总数大于等于电梯最大容量上限,及时满足之前的捎带条件,也不能进行捎带,这样的话一定能够保障电梯内部的人数不会超过最大容量上限。

2.2 bug分析

  此次在强测和互测中我同样也没有被测出bug,在提交之前的调试中我倒是碰到一些bug。

  首先就是双输入端的问题。如果采用的是双输入端的话,就会导致紧接着电梯数之后的那个请求无法被读取。我在自主测试的时候就发现了这个问题,后来询问了一下同学,大概知道可能是输入缓冲区的问题:第一个人的信息,进入到了main函数里面的缓冲区域,而不是进入到了电梯输入的缓冲区,导致第一个人的信息丢失了。很多同学提交之前没有进行相应的测试,而且中测也没有测出来这个问题,导致强测死的很惨。。

  另外一个bug还是和电梯结束问题有关的,原本我还是用的第一次电梯作业对电梯结束的判断,没有考虑到等待调用队列的存在,导致等待队列里还有请求没有分配就关闭了电梯线程,出现了“不载人”的现象。

3、Homework3

  本单元第三次作业进一步对电梯进行规划,规定每种电梯可停靠楼层,由此产生了换乘问题;同时引入了电梯的动态加入的问题,在性能评测的时候话要考虑乘客请求被满足的等待时间。难度相较于上一次作业有所提升。

3.1 程序分析

UML分析

OO第二单元博客作业_第9张图片
OO第二单元博客作业_第10张图片

复杂度分析

OO第二单元博客作业_第11张图片
OO第二单元博客作业_第12张图片

代码行数分析

  本次作业在上次作业的基础之上增加了Person类,用于存储一个人的电梯请求和换乘要求,增加了Floor类,用于存储不同种类电梯可到达的楼层;可以看出此次作业代码还是集中在Elevator类和Controller类中,总结前两次作业的问题,我将Elevator的run方法拆分成不同方法,从而使其满足OPT原则,也不再臃肿,但是分出来的executeMainRequest方法复杂度还是很高,可以继续降低复杂度;和上次作业一样addRequest方法依然复杂度很高。

  本次作业架构基本上和第二次作业保持一致,实现内容上增加了两个重点,一个是通过输入端输入电梯请求动态加入电梯,另一个就是换乘策略。因为会有电梯动态加入,而我一开始的ABC三个电梯是在MainClass里创建的,因此我考虑再三仍然把Input和MainClass放在一起,这样有增加电梯请求的话就直接在MainClass里面创建一个新的电梯线程,同时将调度器可运行电梯数量加一就可以了。

  最主要的就算换乘策略的选择,我选择的是分析理论换乘最优选择,也就是说对于任何一个请求,如果一部电梯能够同时到达该请求的起始楼层和目的楼层,那么该请求不需要换乘;如果没有的话就需要换乘,至于在哪一层楼作为换乘楼层,这个是可以根据不同种类电梯运行速度和到达楼层计算得出的。比如说请求FROM-2-to-16,我可以坐B电梯到1楼换乘,再坐A电梯到16楼,也可以做B电梯到15层换乘,再坐A电梯到16层,这两个换乘策略都可以,但是由于A电梯和B电梯运行速度速度不同,导致换乘方案1比2更快,因此理论上选择方案1是最优的。但是实际运行的时候是不一定的,因为电梯的状态不确定,所以有时候选择方案2更快。因此我认为最优换乘策略需要结合电梯实际运行状态来判断换乘楼层,鉴于这样换乘会很复杂,所以我偷懒没有去实现,单纯采用理论换乘最优选择。

3.2 架构设计可扩展性分析

  在功能和性能设计的平衡上我认为做的还是可以的,本次作业更加重视性能,还要考虑乘客请求被满足的等待时间,在我的设计中,我没有可以去考虑请求被满足的等待时间问题,觉得只要减少换乘次数,先到的请求先执行(类似于FCFS),基本上总的顾客等待时间就不会太长。没有考虑将换乘调度策略和实际电梯运行状态结合也有这一方面的考虑,怕影响到功能的实现架构,从而影响到电梯的可扩展性。

3.3 bug分析

  在此次作业的强测中我没有被查出bug,但是在互测中我被查出一个RTLE,很遗憾的是我在本地跑了很多遍都没有复现出来。出了bug就说明代码是有问题的,但是我仔细思索了很久,并没有发现什么线程不安全的问题,可能我在这部分学的还不够透彻,需要继续研究。

  互测中我用一个比较极端的数据测出来一个人的bug,应该是出现了电梯超员的情况。

二、SOLID设计原则检查

  SRP:在类的实现方面基本都做到了单一功能原则,输入类用于输入电梯请求,调度器用于分配电梯请求,电梯负责执行请求(典型的生产者-消费者模型),但是在某些方法的实现上不太符合SRP原则,比如一开始电梯的run方法。

  OCP:这个原则我认为做的还是可以的,每次作业都是在上次作业的基础上迭代开发,没有出现重构。

  LSP:在本单元的作业中电梯等线程可以通过继承Thread类来实现,其他并没有什么可以继承的。

  ISP:本次作业对象不多,基本不需要实现接口。

  DIP:由于电梯内部有存储自己的状态之类的数据,基本上满足DIP原则。

三、心得体会

  此次单元通过三次作业的练习,让我了解并切身实践进行多线程编程,着重注意了线程安全问题,并了解到如生产者-消费者、观察者模式等设计模式;比第一单元有进步的是这次单元作业都注意到了架构的可扩展性,在迭代的过程中并没有出现重构的情况。

  当然我还暴露出不少的问题,比如自主测试的能力还不足,在今后的过程中还要继续努力。

你可能感兴趣的:(OO第二单元博客作业)