2020_OO_第二单元总结

目录

  •  三次作业架构,实现细节及度量
  • BUG自测与互测
  • 心得与反思

作业分析

  第一次作业

    本次作业是单次可捎带电梯的设计,主要是初步了解多线程的设计实现和测试,本身算法设计非常简单。程序主体由请求接收线程,公共对象请求队列,和电梯线程构成。电梯线程和请求接收线程唯一直接的交互是查看请求是否结束(此处交互的具体处理见心得部分,实际上也是可以避免的),因而十分安全。因为不需要考虑超载等因素,电梯每到一层先查找电梯内队列是否有要出电梯的,再查找电梯外公共队列在这一层是否有人,只要有人,直接放入电梯,不管电梯的运行方向。当当前方向上没有进入电梯的请求,且电梯空时,则转向;当电梯内部队列中每个人的目标都与当前行驶方向相反且当前方向上没有进入电梯的请求时,也转向;当到达顶层或者底层时转向;其余时候都不转向。这种算法得到的最终平均分是97.9分,性能尚可。

  第二次作业

    本次作业将一部电梯改为了有输入决定的1到5部电梯,同时加入了电梯限载的设定。程序主体架构依然和第一次相同,但是公共队列由统一的一个队列变为了分上楼和下楼的两个队列,队列对任何电梯相同,完全是自由竞争。电梯在运行时只进与当前运行方向相同的人,这样使得有限的载荷下运输效率大大提升。算法上,当电梯少于等于2台时,使用LOOK算法,但是具体实现时发现容易和“只进与当前运行方向相同的人”这一点冲突,导致电梯在两层楼之间不断往复的BUG。笔者在这里的解决办法是在look算法中加入缓冲层,即检查的是当前楼层及运行方向上的楼层是否有进入电梯的请求。当电梯总量大于2时,直接使用scan算法,每部电梯都在顶层和底层间扫描,同时调整初始运行方向和第一次上升的最大高度,保证电梯能够错开。最终平均分是98.9分。

  第三次作业

扫描二维码关注公众号,回复: 10906286 查看本文章

    本次作业将没有了通用电梯,而是改为三种不同类型的电梯,分别只能在特定的楼层停靠,这就导致了有些请求必须通过换乘来解决。同时增加了动态生成电梯的指令,不过这一部分实现比较简单。程序主体架构仍然是和之前相同,但是调度器维护队列的任务急剧增加。关于换乘,笔者主要考虑的是实现的简洁和绝对随机数据下的均衡。在这样的考量下,笔者选择了最多一次换乘的固定调度策略,即每一种请求有且仅有一种实现方法,当请求确定时,其是否需要换乘和怎么样换乘就都确定了。具体的换乘实现是,请求处理线程接收请求后不做分别直接调用调度器的put()方法,由调度器自身对请求作识别,如果是需要换乘的请求,则将请求的前一部分放入自身维护的队列中,同时换乘计数加一,作为电梯线程结束或休眠的判断条件之一。这里的队列分为6个,即3类电梯上下楼都有各自单独的队列。当各类电梯走到特定的楼层时,会检查换乘:将需要在当前楼层换乘的人移出电梯,同时调用调度器的put()使其进入到请求队列(环衬策略确保不会产生新的换乘,即从电梯出来后没到目的地的人,再乘一次一定能达到),换乘计数减一。电梯的运行策略是LOOK,最终平均分为98.8分。

  关于SOLID等设计原则分析(可扩展性见反思部分):

    SRP:糟糕透了,每个模块都是一个类搞定,电梯和调度器有数十个方法,几百行的代码量,没有进一步细分其各种功能。

    OCP:想了想应该是不可能了,调度算法深入了电梯调度器的每个类中,只有请求处理类可能做到。

    LSP:做到了,因为根本没有自己设计的继承关系...

    LOD:电梯类和请求处理类基本没有直接通信,也就没有直接调用。

    ISP:只实现了RUNNABLE接口,显然是必须的。

    DIP:没有设计好抽象层次。

BUG自测和互测

  自己程序的BUG

    三次作业的强测、互测均没有被发现bug,可以说是非常幸运了。

  hack他人

    秉持着人不犯我我不犯人的原则,仅对程序基本的功能作覆盖性测试,以及利用一些边界数据做性能上的检查,并没有自动生成大量数据捕捉特殊的线程安全问题(主要是技术手段有限)。性能上的检查也并没有针对某种特定的算法捏造数据,一是因为时间限制确实给的宽,二是将心比心,用这种这种不太算BUG的BUG搞同学心态似乎也不太好,因为确实有些同学的调度算法在绝大多数情况下性能很好,但是个别极端情况下会在正确运行时产生TLE,这只能说是一种权衡,不能算作错误。结果来看,第一次全房间无伤;第二次房间唯二两刀中,笔者贡献了一刀;第三次房间战况激烈,但是只有一位同学是在功能上出错,成为了集火的对象,其他同学只在个别时候出现死锁等安全问题,考虑到房间足够活跃,笔者选择了远离战火,以0/0,0/40结束了互测环节。

 

心得与反思

  本单元电梯作业的主题是认识多线程,尝试应用一些关键字、锁的机制保障线程的安全。三次作业的架构中,唯一的线程间直接交互,是电梯线程访问请求处理线程,判断请求输入是否结束。在这里有一个有趣的陷阱:每当电梯运行到一层,完成上下人以后,会访问请求处理线程,若发现输入还没有结束,但是现在公共等待队列是空,且电梯内没有人,则会wait。而请求处理线程发现输入结束后,会notify所有电梯。初看好像还行,但是如果在电梯访问请求处理线程得到输入没有结束时,输入正好结束,则有可能请求处理线程先notifyAll,然后电梯线程再休眠,从而导致程序不能正常结束。解决方法是在电梯线程中把访问请求处理线程的过程和wait一起锁在调度器上,同时在请求处理线程中把修改输入结束标志的过程也锁在调度器上。

  在线程安全方面,为了避免一个线程直接访问另一个线程,笔者的调度系统不得不放弃观察电梯的状态,只能采取同类电梯自由竞争的策略,虽然最终取得了不错的性能分,但是有逃避问题之嫌。现在想来,可以通过新建status board公共对象,完成对各个电梯状态和请求输入状态的记录,给其他线程访问。

  在实现拆分策略时,涉及到了多维度的分支嵌套,复杂度很高。第一次写该模块式时,为了节省行数,没有用层次化的分支展开,而是完全并列的分支,这对代码的检查和拓展制造了麻烦。后来以请求的进入楼层为分类依据,每个类单独建立拆分方法,根据目标楼层进行拆分,并完成将其加入至相应队列的任务。此外,idea会对分支判断中的冗余条件进行检测,并且提示删除,但笔者认为,删除冗余条件,会使得没有注释的情况下代码的阅读更加困难。

  在加锁的方式上,笔者全程使用synchronized关键字,虽然很稳,但是缺少了对其他方式的体验和了解。如果不是涉及到作业分数,体验更多的方法可能会更有收获。除了物理锁,也从各种途径了解到了各种逻辑锁的设计,但是缺乏实践。

  在抽象层次设计和设计模式运用上,还是有很多遗憾。这个单元的架构参考了生产者-消费者模式,但是由于架构的设计,对于很多同学使用到的观察者模式,笔者并没有需求。主要的遗憾是在抽象层次上,除了线程必须继承的runnable接口,再无接口的使用,同时继承的设计。特别是作业3的3种类型的电梯,实现前没有完整的想法,只有过程化的构思,导致没有办法抽象出一个超类。虽然说并不会涉及到使用超类的容器进行统一的电梯管理,但是在考虑到代码的复用性,这样做还是很有问题的(写完以后发现3类电梯大部分代码都是相互复制的,而且检查进出人、检查换乘、检查转向等方法的划分,使得其中许多方法在三种电梯中是一致的,完全可以通过在超类中定义)。

  关于性能与优化,这单元的作业,尤其是后两次的作业,对于笔者来说,高性能的思路非常难确定。很多策略之间不存在绝对的性能碾压,在不同倾向的数据下(具体涉及到请求的密度,请求的种类分布等等),性能互有优劣。因为不知道数据集的具体情况,可能你认为的优化其实不是优化。这里笔者有一个很典型的经历:最后一次作业笔者曾经尝试将新增的电梯由LOOK转为SCAN,在自己构造的测试中,10组数据有7组是变快了,但是中测样例中涉及到新增电梯的几乎每一组数据都变慢了。此外,部分非自由竞争的调度模式,可能会被针对性地捏造数据攻击,即出现大部分时候都有着很好的性能,但在极端情况下却会TLE。总之这个单元的作业,性能优化是一个涉及到很多trade-off的过程

  关于测试和调试,前两次作业完成了时间监测以外的全部评测,第三次由于时间原因,只对换乘目标达成和电梯基本逻辑进行了检测。但是由于对命令行和Python了解有限,仍然没有实现自动投放数据的评测模式。同时,从最近一次的研讨课上了解到,可以利用IDEA内置的单元测试功能完成类似testbench的测试文件,对单个类的功能进行测试;此外,可以利用java中的内置类ThreadInfo等,完成对CPU使用时间,锁的持有情况等进行监测;debug logger也是一种不错的调试方式。

  

猜你喜欢

转载自www.cnblogs.com/xmy1225/p/12701897.html