面向对象课程第二单元总结

 本单元的整体多线程冒险体验对于我来说是要比第一单元好的……原因无他,我第一单元栽倒在输入处理和正则上的比例有点高,感觉除了设计上的丑陋之处,其他的全是输入处理上的问题= =……

  当然这并不代表本单元的冒险不紧张刺激,但至少省下输入处理的精力,我在代码结构的构造上面还是下了功夫的,整体构造感觉上要比第一单元更加合理一些(也许代码度量插件不这么想orz)。这其中也有老师教的合理的多线程设计模式的功劳,在已有的框架内,会把生产者消费者、仓库工人等等线程类所要完成的工作规定清楚,避免上帝类、废物类、奇怪线程类的出现。

  另外的收获就是对于多线程同步机制以及线程间通信的感悟。尤其是第二次作业为了照顾设计成丑八怪的调度算法,写了个二重生产者消费者机制,线程同步挂了很长一段时间才调好,具体还是留到底下作业分析之中细说。

 

第一次作业

 

  总体设计使用生产者消费者模式,将输入作为生产者,将电梯作为消费者,将调度器作为共享对象。

  算法设计:由于规定是傻瓜调度,所以我并没有在算法优化上下功夫,只是进行了优化的思考,怕为了一口气吃个胖子把代码搞错。生产者没啥好说的,channel类也和课上介绍的差不多,消费者采用拿不到指令就wait(),拿到指令直接傻瓜调度跑完的方式,所有的输出一并写到了消费者类中。

  测试结果:强测以及互测均未出现问题。

    本次作业较为简单,方法很少所以不做度量。

  类图和统计数据如下:

 

第二次作业

 

  总体设计使用一个非常杂技的双重生产者消费者模式, 生产者给仓库指令,消费者取走并自己维护一个队列(由于涉及到后续处理,必须要维护一个队列),然后把指令分为两部分,一部分作为可捎带指令直接喂给电梯线程类,另外一部分作为不可捎带指令存入一个叫做requestbuffer类做缓冲,再由电梯线程成批次取走。

  算法设计:整体实现上非常类似于look算法(只是类似,毕竟结果显示我算法还是写错了一些东西的),逐层进行指令的扫描,消费者队列为空时则去requestbuffer抓指令,抓一次把同方向的一批都抓走并从最远端开始运行电梯(向上就是最下一层,向下就是最上一层)。想的还是比较周到,但实际上构造起来过于杂技,缺点也比较明显。比如look算法没考虑队列所有指令,只按队列第一条确定下一步运行方向,在性能分上完全得不到保证;把电梯拉到最远端的初始化方法还很容易出错,比如说初始化的同时进入了一个由消费者线程喂过来的同向路上上车的指令,我就完全无法在本架构上处理(后来BUG中发现的,大改了一番才处理好)……类似的算法也在第三次得到了优化。

  测试结果:初始化的同时进入了一个由消费者线程喂过来的同向路上上车的指令无法处理导致了强测互测的一半错误,另一半是某个布尔变量初始化错了,导致按批去缓冲区抓来的指令有可能无法被我设计的初始化处理……actually,错误比较微妙,乍一测不好测出来,我当时对程序有些过于放心了,想的太少……

   类图和统计数据如下:

代码度量如下:

 

第三次作业

 

  总体设计使用了课上介绍的work-thread模式, 生产者给channel指令不变,由main函数初始化三个lift线程,各带着最大载客量、运行速度、运行楼层这样的数据进行初始化;然后由三个电梯线程扮演消费者,如果channel没指令或者没取到自己能处理的指令则进入wait()状态,由“向channel队列里放指令”这一动作唤醒;然后每个电梯都采用LOOK算法(本次未对算法进行改良,除了保证了正确性),逐层去channel抓一遍指令。

  算法设计:关于LOOK的部分就不详细说了,和上次的基本想法是相同的,但代码重构后应该不会出现上次的错误。关键在于本次的指令分配与指令切割,我的基本构想是这样的:

  1. 首先给指令分类,我定义了三个布尔变量fora,forb,forc,意味此指令可以给谁。

  2. 判断过程为:如果某指令可以让某电梯全权处理,则给这个电梯,(但不独占,比如1楼到15楼,fora、forb、forc都是true);如果不能直达则分着看本指令from的楼层是哪些电梯能到,能到的电梯可以先拿走运行。

  3. 由于必须切割的指令就算让某个能接上的电梯拿走,也处理不完。这是后看TOfloor是哪个或哪些电梯能到的,比如-3层到4层,4是B的运行区域,然后我会找电梯运行方向上最近的AB交互楼层作为指令切割点。

  4. 拿刚才的-3到4层举例,指令刚被A拿走时我会进行检查,如果A干不完马上找切割点,找到后一分为二,变成-3到-2,-2到4两个指令,后面的指令作为tag和前面指令一块储存(能一口气干完的指令TAG为null),然后逐层运行,等到了-2层,-3到-2指令运行完毕踢出队列的同时,检测一下TAG是否是null(从上文可知不是),然后将TAG中的指令直接扔回channel类的队列中,作为新指令等待新一轮的foraforb检测。

  5. 以上的理论基础是建立在:所有指令都最多需要两个电梯处理;如果一个指令需要切割,那么在电梯运行方向上一定有切割点存在。

  本次的算法设计其实并没有考虑到效率的问题,只是一个保证正确性的设计。后来我看到讨论区很多大神也分享了如何优化怎么处理的,也领悟到了很多。所以其实还是有很多优化的空间存在,比如:LOOK算法的运行方向的考量;运行方向上的切割点是否是效率高的(比如2→3这样的指令,我会切割为2→5,5→3,肯定不如2→1,1→3)……所以其实整体来说是很保守的设计。

  测试结果:emmm本次程序出现了比较严重的“笔误”:上交的版本丢了个运算符。在自己BUG修复找到这个问题后,所有错的点全部正确了。所以应该说在思路以及代码构造上正确性还是有保证的(大概),更多的不足应该是没能更进一步考虑性能分。

 类图以及统计数据如下:

代码度量如下:

这里要说一下,Define类是我写的一个指令辨别类,里面的所有方法都是数据处理,为其他类服务的,写的时候没动脑子扔了许多循环嵌套进去,导致规格严重超标,而且不好改。

另外LIFT类则是沿用的上一次的算法没做修改。

所以在本次设计思路较为清楚的情况下,规格仍然炸了……这是我今后会更加注意的地方。

根据SOLID原则进行总体分析

SRP - 单一责任原则:就第一次作业来说,由于是傻瓜调度所以我把电梯运行过程写在了消费者线程内,不符合SRP;第二次则是用的设计模式过于混乱,其中消费者→缓冲区→电梯的结构对SRP也有些许不符合的情况;最后一次就方法分布而言实现较好。

OCP - 开放封闭原则:对扩展开放,对修改封闭,这点如果要总结的话,大概还是第二次作业会有一些问题,其实在指导书以及课件上已经说明了要想多电梯做结构上的准备,如果当时能够多思考一下如何扩展,可能就直接向 work - thread 设计模式靠拢了,也不至于写出那个结构来。所以总体总结来看三次作业都有不同程度上的重构。

LSP - 里氏替换原则:所有基类出现的地方都可以用派生类替换而不会程序产生错误。本单元没涉及到继承(或者说可以不用到继承),所以并未在这个原则上有所冲突。

ISP - 接口隔离原则:类不应该依赖不需要的接口,知道越少越好。我个人理解这一条原则应该和SRP有很大关联,接口隔离的前提是功能与类设计的合理,当然本系列也不涉及接口问题。

DIP - 依赖倒置原则:高级模块不应该依赖低级模块,而是依赖抽象。类A内有类B对象,称为类A依赖类B,但是不应该这样做,而是选择类A去依赖抽象。这个在设计的时候没有考虑,所以几次作业都没有实现这一点。


BUG分析

  上面提的差不多,在这里总结一下:第二次作业是本单元的BUG集中地,第一次较为简单以及第三次写的保守,无明显的业务逻辑BUG。

  主要分为两种BUG:1. 初始化的同时进入了一个由消费者线程喂过来的同向路上上车的指令无法处理,这应该是算法和程序结构设计上的失败。第二次本身的多线程模式就被我写的比较杂技,再加上测试的不充足,没想明白什么时候可能会出现捎带的情况,最终导致了这个BUG出现。

  2. 某个布尔变量初始化错了,导致按批去缓冲区抓来的指令有可能无法被我设计的初始化处理,这应该也是自己测试样例不充分的锅。

 

互测环节分析

   说几个在本单元测试中学到的点或者是需要总结的点:

  1. 程序的正确性要优先于性能分(分奴本质在第二次设计蛇了以后得到确认……),如果要追求性能分,请在一开始设计清楚,后面边写边优化极容易出现觉察不出的错误。

  2. wait 和 notifyall 可以解决本单元的所有问题,因为本质上本单元设计所操作的线程不会超过5条(因人而异),所实现的线程类不会超过三个,所以同步起来还是比较条理清晰的。

  3. 只要不写轮询结构,CPU时间不会超时。第三次作业我写了个DIFINE类非常吃CPU资源(本意是给指令定性,但是写的太急了,把各种循环一并扔了进去),但是那也不会超过1.5秒(相对于大部分同学的1秒内解决已经很多了)。

  4. 不要想当然的判定各线程的运行状况从而写出一些不留退路的代码。举个我自己的例子,在某次写代码的时候我同时start了生产者和消费者,消费者循环取,取不到就wait,我想当然的认为一上来消费者是必然wait的(运动员同速赛跑的思维),所以给wait的代码块里面后加了个电梯初始化。实际上,如果你生产者第一个指令给得快, 消费者是可以在没运行到wait的时候仓库中就有了指令,到了扫描仓库直接取即可,不进wait的代码块,程序就有可能无法使电梯初始化……

 

心得体会

  1. 设计永远优先于编码,错误有一半是来源于设计不周,这点在多线程这一章节比第一单元更加明显。

  2. 多线程的调试,脑子清楚是十分重要的,该画图就画图,该print就print一下,笨一点没关系,最怕想当然或者乱改一通。盲人摸象能摸全,也比正常人见都没见过画出来的好。

  3. 重构不是火葬场,但是考虑到工作量,还是要避免。重构其实是可以让人学到一些东西的,但从完成作业的角度来看,有一个可扩展性的底子顺水推舟做扩展,要更实际一些。

  4. 最后感谢课程组提供的输入输出接口以及讨论区大佬提供的测试方法和测试黑箱接口(这个群就我是弱鸡.jpg),第一单元的正则和输入处理真的是出错出到想砸键盘……

 

posted on 2019-04-20 00:19 xsndzxc 阅读( ...) 评论( ...) 编辑 收藏

转载于:https://www.cnblogs.com/xsndzxc/p/10739622.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值