OOUnit2Summary

一、前三次作业内容分析

前言

第二单元的作业以多线程为主题,以电梯调度为背景,分三次要求逐步增加,难度逐步提高。这三次作业,更新了我对于面向对象编程的认知,也进一步提高了我编程和调试的能力。

一下是我对于这三次作业的总结,希望能给我以后得到作业和今后的编程道路提供帮助和参考。

注:

1.由于三次作业下来,本人代码的整体架构,没有变化,仅仅调整了部分方法,三次作业的时序图完全一致,仅仅给出了第三次作业的时序图。

第一次作业

作业要求简述:单部多线程可捎带电梯,不限定电梯调度逻辑,只要输出符合现实逻辑并满足最大时间限制即可,同时为了防止轮询限制了CPU时间。

简要思路:第一次作业要求相对简单,是为了帮助我们熟悉多线程程序的编写。

整体架构上,本人构造了三个线程( Main除外):Input线程,Elevator线程,Control(线程),三个线程共享一个Disposer(调度器对象),通过里面的方法存取公共数据(内外Person队列)。三个线程分别负责输入请求的读取,电梯开关门和上下楼,人员的进出。

调度策略上本人采用了指导书上推荐的ALS规则,每次循环都找到一个主请求,按照先来后到原则为每个人设置一个优先级,电梯内的人比电梯外的人优先考虑,主请求的目标楼层即为电梯的目标楼层,可捎带请求的目标楼层不影响主请求。

类图:

复杂度分析:

性能分析:本人采用的是ALS调度算法,性能上并不是最优的,因为主请求的路线不一定是最优的路线,综合当前电梯内外人员情况设计路线并时刻更新才是最优解,因此本人性能分虽然可观但不是非常理想。

Bug分析:这次作业出现的问题,大多集中在轮询CPU超时,产生逻辑死锁,线程为退出等方面,而不是输出不符合基本逻辑。本人在中测中也出现了这样的问题。之前本人认为只要在电梯和控制线程在内外队列无人的时候进行wait即可避免轮询,但实际上这样是不对的,比如当电梯在上下楼的时候,就不满足人员进出条件,此时控制线程应该在wait而不是反复循环判断是否有人员进出条件。好在中测让本人及时发现了这个问题。其实这样的问题根本上来源于本人多开了一个控制器线程,将其封装起来独立于elevator,但这样的操作对于第二次作业的迭代开发十分有利。

第二次作业

作业要求简述:为多部多线程可捎带调度电梯,即在上次作业的基础上增加了多部电梯的要求,同时增加了负数楼层。

简要思路:第二次作业的主要难点是多电梯的调度,但如果第一次作业的架构好,迭代开发上会较为轻松。

整体架构上,本人构造了三个线程类( Main除外):Input线程,Elevator线程,Control(线程),一个Elevator有多个对象,对象之间完全等价,所有线程共享一个Dispoer(调度器)对象。

调度策略上本人没有多大变化,只是从单部调度变为多部调度,这样就涉及到电梯竞争资源的问题。本人设计的思路是距离优先,谁离得近就谁去接(不影响主请求的前提下),距离相同则人数少的去接。

类图:

复杂度分析:

 

性能分析:多电梯的调度下,宏观的人员分配十分重要,甚至比单部电梯的运行策略更为重要。虽然本人在单步电梯上不是最优解,人员分配的算法也十分粗糙,但多级循环的遍历操作让算法复杂度陡然上升。好在本人发现,电梯数目越多,粗糙的调度策略和遍历后的最优策略差别就越小,所以这次本人的性能分还是比较理想的

Bug分析:本人在这次作业上出现了奇怪的问题,至今都未彻底理解其原因,这个错误在互测环节被爆出,但强测环节20个点一个都没有测出这个问题,甚至本地测试都正常运行,无法复现,以下是本人经过试验(折腾测评机)得出的可能的解释。为了防止产生死锁,本人在每个线程wait之前都进行notifyAll。但实际上这样做会导致线程之间轮流唤醒和被唤醒(本人也不理解为什么),条件判断完毕后继续wait,导致电梯数目多会产生runtimeerror(个人觉得有错也应当是ctle),这个错误在互测环节被爆出来,但本人在本地测试却复现不了这个runtimeerror。好在本人发现出错的互测数据都是电梯数目为5,其中一组数据只有两条指令,第二条和第一条之间隔了60s之久,电梯线程在这期间并没有wait,而是相互之间notify,频繁的开锁关锁导致了错误(具体错误本人还不是很理解,由于任务紧暂时没能深入研究)。最后本人删掉了一些方法中不必要的锁,不必要的notifyAll,有的notifyAll加上了if条件,解决了这个问题(但还是不太理解)。

第三次作业

作业要求简述:多部多线程可捎带调度电梯,但在第二次作业的基础上增加了电梯类型和可停靠楼层。

简要思路:第三次作业的主要难点是可停靠楼层,这意味着电梯之间有可能需要分工合作,才能把乘客送往最终目的地,这样一来,调度的难度在上一次的基础上进一步增加。

整体架构上,本人与上次的变化不大,只对部分类中的方法进行增加和调整。

调度策略上,本人也没有多大变化,但是为了适应新增的要求,本人设置了人的状态这个新变量,0代表未上电梯,1代表直通搭载,2代表中转搭载,中转搭载需要在下电梯的时候重新加入到外队列中。值得注意的是计算中转楼层的算法,本人选择当前电梯的可停靠楼层中距离目标楼层最近的一层,但需要注意排除盲点(其他电梯都到不了的楼层),同时计算出的中转楼层就在当前楼层的人不应当进电梯。同时,在人员的分配上本人也做出了一定调整:一开始,本人着重考虑直通的电梯,减少中转的情况,但这样实际上是不对的。每个电梯都有盲区,中转的情况是绝对不可避免的,所以应当让距离和主请求方向的优先级高于直通;说的通俗点就是人不应该在原地等待能够直达的电梯,而是先尽量搭载可中转的电梯。但更为优秀的算法应当是在两者之间通过计算进行权衡,但恕本人能力和时间有限,难以进行这样的操作。

类图:

 复杂度分析:

 

 

性能分析:这次作业相对于前一次只不过增加了人员的中转,本质上任然是多部可捎带电梯,因此只要中转搭载逻辑设计的合理,性能上拼的还是第二次。本人在这次作业中凭借简单朴素,正确性为上的调度算法拿到了不错的性能分。

Bug分析:这次作业代码的修改度是最少的一次,有可能是这学期最少的一次,但这一次本人遇到的bug前所未有之多,也出现了上次作业那样的奇怪的bug,但好在本人bug修复和新的作业并行,让这个bug没能流入强测和互测环节,以下是本人提交中测前后自测出的一些bug:

1.电梯将人送到其它电梯的盲区,由于这一层离乘客目的地始终最近,导致这个人反复从这一楼进进出出,却始终到不了目的地;因此,本人在计算中转楼层的函数中对返回值进行了特判,但如果课程组增加难度,将可停靠楼层改为未知,本人将原地爆炸;

2.由于距离最近,电梯把已经送到中转楼层的人又接上了电梯,但计算出的中转楼层还是这一层,也会导致乘客在这一层进进出出;因此,本人在接受中转乘客的时候排除掉那些计算出中转楼层就在起点的楼层;

时序图和第三次作业的可扩展性分析

时序图

由于本人三次作业的整体架构差别不大,每一次的迭代都是对一些方法进行调整,所以画出来的协作图基本一致,这里只提供第三次的时序图:

由于本人的Elevator线程是在Input中创建并运行的,这里给出两张时序图。

由图中可以看出,本人先再主线程(MainClass)建立了作为共享对象的调度器(Disposer),再创建输入(Input)和控制(Control)线程,当输入线程开始运行时,会首先创建三个电梯(Elevator)线程,在随后若遇到新增电梯指令时会创建更多的电梯线程。

可扩展性分析

对于新增的要求,本人也只能见招拆招,所以可扩展性的分析本人可能不是特别准确。

在整体架构上,本人代码的可扩展性还是比较理想的,在第一次作业开始(之前),本人就参考了往届大佬的博客,改变了原本将调度逻辑写在电梯里面图方便的目光短浅的想法,将调度逻辑放到了共享对象里面,甚至一不做二不休,把电梯的状况和人员的状况等也作为共享变量,电梯只通过相应的方法去访存。这样的操作大大提高了代码的可扩展性,对于新增的要求只需修改共享对象里的方法即可,不需要对整体架构大动干戈。

功能和性能的平衡上,本人其实是做的很不好的,为了保证功能正确,本人在一些方法中对队列反复遍历,反复判断,这使得代码性能大打折扣,因为每一次的调度,都是复杂度极高的循环遍历。同时,本人为了保证功能上的正确,没有像一些大佬一样通过计算可能解求最优的方式(怕玩脱),而是粗爆地按照距离>直通>人数的优先次序来调度电梯,但好在这种考虑适用大部分情况,让本人在性能分上十分可观。

设计原则上,本人做的其实有欠缺,,在此逐一分析:

1.SRP,本人将人员的进出,电梯的上下,门的开关放在共享对象的不同方法中,每个方法只实现一项功能;但有的方法中包含多个功能,如人员的进出本人放在了一个方法,但由于涉及到较为复杂的调度逻辑,这部分的代码较为臃肿,把进出逻辑分开,再综合到一个顶层方法中,是更好的选择。

2.OCP,当从单电梯向多电梯过渡时,本人只需修改上层的调度器的调度逻辑,电梯本身不需要修改;如果在第三次的基础上增加要求,也只需修改或在原来调度器的基础上继承即可;

3.LSP,ISP,由于这次作业的架构相对简单,本人没有用到多少类的继承和抽象接口;

4.DIP,本人直接将电梯的状态设置为公共变量,由调度器统一管理,这样一来,位于顶层的调度器不需要反过来读取位于底层的各电梯的状态决定调度策略,分派任务,而电梯需要依赖调度器中存储的自身状态来决定开关门和上下楼。

二、bug分析

自己程序的bug

本人在上一节内容中的每一次作业都分析了自己自测或被测出的bug,因此这里不多加赘述。

发现别人bug采用的策略

这一次作业,本人仅仅看了其他人代码的大致架构,然后自行构造数据进行测试,因为这一单元大多数的bug集中在线程安全,死锁,轮询等隐秘调度方面,肉眼看出是不现实的,只能通过多组测试,单组复测等方式查出有bug,再进一步调试和分析。

由于本人不会自动化构造,只能手工构造数据,在数据构造的策略上,本人要么在同一时刻增加多个请求,或是长时间不增加请求最后突然加很多请求,并且增加的请求多是在电梯的盲区之间进行穿梭,这样可以增加中转的可能,这与第一单元的肉眼查错加精准构造截然不同。

为了适应实时输入的特性,本人设计了一个Inputtest(测试输出)类,将所有请求放到一个队列中,将时间戳存放的double数组中,再反复轮询读取当前时间,若与数组中的时间匹配,则增加到电梯的请求队列中;同时,本人写了一个测试输出是否符合逻辑的类,这其实很容易,只需枚举出所有输出不符合逻辑的错误即可。

三、心得体会

这次作业刷新了我对面向对象编程的认识,使我受益匪浅,多线程的玄学bug也提高了我的调试能力和心理素质。

线程安全上,本人十分谨慎,对所有访问共享变量的方法都进行加锁(某些嵌套在单个方法中的为了分担长度的方法除外),若方法对共享变量造成了改变,则在末尾及时notifyAll,这样可能比较稳妥。

设计原则上,本人提前参考了往届大佬的博客,提前确定了比较优秀的设计架构,因此比较符合理论课上所提的设计原则,但在一些细节上由于代码经验尚浅,违背了一些比较优秀的原则,而由于本次作业内容的原因,一些原则并没有得到体现或者不是特别明显,但我相信它们在我今后的作业和编程之路上回有所体现,给我带来帮助。

posted @ 2020-04-17 17:35  mrqy  阅读(140)  评论(0编辑  收藏  举报