OO 第二单元

前言

​ 第二单元 OO 作业的主题是多线程,课程组通过了电梯调度这个经典问题考察了多线程的调度。

​ 从第五次作业到第七次作业的迭代为,单部多线程可捎带电梯多部多线程可捎带调度电梯(电梯属性相同)多部多线程可捎带调度电梯(电梯属性不同,包含需换乘请求)

一、作业分析

作业分析将通过代码复杂度度量,UML 协作图,扩展性,优缺点等方面进行分析

第五次作业

​ 第五次作业的任务是完成单部可捎带电梯的调度。

设计分析:
  1. 第一次多线程作业中,我初步设想的是有三个线程,一个输入线程,一个调度线程,一个电梯线程,三者进行互动完成调度;

  2. 设计了BuildingFloorPerson三个类作为存储单位,帮助自己更好得管理输入请求和电梯楼层的人员状态;

  3. 采用”生产者-消费者“模型,其中Building类作为托盘,供输入线程传入信息,控制器线程获取信息(第五次作业因为单步电梯这种设计有一定的局限性)

代码复杂度分析:
  1. 很明显在Elevator类中方法的总循环复杂度偏高,是因为我把电梯的行为都设计在电梯之中,暂时没有想到很好的抽离出电梯行为的方法;
  2. ElevatorController两个类之间的耦合度过高,控制类因为需要调度电梯类,需要过多的电梯内部信息,这种交互模式不利于后续的扩展。
优缺点分析:

优点:

  1. 请求按照类来存储,在管理上十分便利,如果需要扩展也有着很高的扩展性;
  2. 采用了lock的锁的方式,个人感觉相较于synchronized更容易维护线程安全和修改,其次不关联的共享变量可以自己多创建几个锁,也方便自己的理解和使用。

缺点:这次作业的问题其实挺多的,刚刚接触多线程,对很多概念其实不太了解。

  1. 为了安全性,我把所有的方法都加上了锁,就看起来很累赘;
  2. 电梯和控制器的耦合度过高,不利于扩展和多电梯的调度;
  3. 电梯类的run方法里面就是一个停止的特判条件,容易产生 ctle,在互测的时候被hack了。
性能分析:

采用了 look 算法,思考角度是从生活的电梯实际运作情况出发,性能表现在预期之外;在研讨会的时候有同学分享了贪心的算法,在电梯人数没有限制的条件下确实是最优的方式。

第六次作业

第六次作业的任务是完成多部可捎带电梯的调度,这些电梯的属性都是一样的,可以到达所有楼层,其次有载客量等限制的出现。

设计分析:
  1. 与作业五相同部分不再赘述;
  2. 将三个线程控制改为两个线程控制,仅留下了输入线程和电梯线程;
  3. 控制器作为输入请求的分配器,来合理分配输入资源;
  4. 电梯类中存储专属的Building托盘,用于存储调度器分配的请求;
  5. 电梯类自行调度,从Building中获取资源,等输入结束后,由调度器通知所有电梯输入结束;
  6. 电梯类自行调度全部采用 look 算法;
  7. 添加了State的枚举类来帮助自己更好地管理调度器状态和电梯状态。
代码复杂度分析:
  1. 将电梯的调度从调度器移植到电梯内部,具有更好的可扩展性,但是相对应的电梯类的复杂度也持续攀升,之后如果还有需求,可以将电梯的行为再次抽象,降低复杂度。
优缺点分析:

优点:

  1. 设计逻辑更加清晰,更合理的状态管理和资源的分配;
  2. 控制器变成资源分配的中介,便于扩展和维护,虽然这样性能上可能会下降(但是实际却性能更好了,我也不知道为什么)。

缺点:

  1. 电梯类的方法可以进一步抽象,降低复杂度。
性能分析:

我采用的是单步电梯 look 算法,请求按照当前电梯请求数最少来进行分配,效果超出预期,可以说相当不错吧;与同学交流的时候,得知了另一种调度方式,就是请求资源的完全竞争,这样确实能够保证总的性能最佳,不过和他们差距也不大,觉得自己的设计可能在一定程度上更贴近生活。

第七次作业

第七次作业的任务是完成多部可捎带电梯的调度,这些电梯的属性是不同的,除了载客量和电梯运行的速度差异,在可停靠楼层上也有不同,有的请求需要换乘才可以到达。

设计分析:
  1. 与作业六相同部分不再赘述,作业六其实是个很好的框架,自己扩展第七次作业包括设计一共不到1小时;
  2. 因为电梯属性不同,添加了电梯工厂类帮助自己更好地创建电梯对象;
  3. 添加了安全输出工具类(因为课程组的要求)。
代码复杂度分析:
  1. 因为换乘的新要求的加入,电梯类需要对当前人员能否直达和是否需要换乘进行判断,电梯类再一次复杂度升高,因为最后一次了,自己也就没有花时间去进一步抽象了;
  2. 调度器类中,有个换乘和电梯选择的比对,所以复杂度也上升了。
优缺点分析:

优点:

  1. 电梯工厂的出现,帮助自己更好地完成了扩展;
  2. 在最初作业中的储存信息对象的创建帮助自己在最后一次扩展的时候节省了很多时间,只需要在相对应的Person或者Building类中添加相关属性,就能很好地完成扩展。

缺点:

  1. 类的复杂度太高。
性能分析:

因为电梯属性和换乘的新需求的加入,我对调度器请求分配的优先级进行了比较,他们的电梯完成请求时间近乎一致,但是在性能新标准人员等待时间上能够差近 30s,我选择了我测评机中最优的电梯优先级CAB,在换乘上我采用的是最近换乘点就放下的策略,但是这样的换乘策略不是最佳的,最佳的换乘策略应该是只有1 5 15(在我的架构下)。

分析小结

通过 SOLID 原则对代码进行分析

  • SRP:单一职责原则。我认为我这三次作业迭代下来,在这个方面在慢慢做好,从开始的控制器类和电梯类的高耦合,到最后分离耦合度,建立工厂,其实整体是在慢慢变好的,但是这个职责没有完成地很好,在电梯类中表现地很明显,电梯类除了自己的内部资源的维护,还需要兼顾调度,交互等多方面的工作。
  • OCP:开闭原则。这点没有实现得很好,每次新需求的添加,同时通过修改原来类的方法实现的,没有很好地用到继承等方式来降低电梯类复杂度。
  • LSP:里式替换原则。没有涉及继承和接口实现,没有体现本原则。
  • ISP:接口隔离原则。没有实现接口,没有体现此原则。
  • DIP:依赖倒置原则。没有很好地实现,过度地依赖实例。

​ 基本调度逻辑。

二、Bug与性能分析

Bug

自测:主要通过自动化测试以及极端数据测试

  1. 在第五次作业中,出现了死锁的现象,主要是因为notifyAll,之后将所有同步用lock的方式实现;
  2. 在第六次作业中,自己电梯的人数限制上除了问题,会出现超载的现象;
  3. 在第七次作业中,存在换乘人员还没换乘成功,其他电梯就停止的问题。

公测与互测

公测:没有出现bug

互测:第五次作业中被hack了一次 ctle,之后通过JProfiler检查出来确实有轮询的问题存在,之后就舍弃了这种设计框架

性能

​ 准确来说这三次作业的性能分都比自己的预期高很多,自己其实没有进行很特别的优化,尤其是在第六次作业以后,调度器作为请求分配的主体,电梯只服务其等待队列中的请求,而电梯本身都是按照 look 算法的方式进行调度的。

​ 我在调度策略上使用的是比较均衡的调度策略,按照每部电梯实时请求数量进行分配,在课下选择调度策略的时候,通过对多种调度器分配请求的算法进行了比较,选择了最为稳定和均衡的调度策略,最后跑出的结果确实超过自己的预期。

三、互测策略与测评机

互测

​ 互测方式主要是通过测评机跑和极端数据测试两种方式。

​ 测评机在这个单元用处除了自测的时候帮自己找 bug 外,在互测阶段只在第五次作业中帮自己 hack 到了人(搭测评机所花的时间比自己写 oo 作业的时间还多),可见能够通过强测并取得不错成绩的同学在正确性上都没有大问题。

​ 极端数据测试。这个是帮助自己 hack 到最多人的方式,启发来自第五次作业被人用超长时间间隔数据输入 hack 出 ctle。自己的自动化测试每次跑的请求数量在(35,45),时间间隔控制在 2s 以内,所以对于特定的 bug,如 RTLE 和 CTEL 都没有有效的 hack,只得自己构造特殊数据用测评机测试才能发现这些隐藏的 bug。

​ 在查看别人代码时发现,有人使用轮询,但是采用了sleep(1)这种方式来减少 cpu 的使用时间,然后我用了间隔近 60s 的两组输入来对这种 CTLE 进行 hack(超级加倍)。

测评机

​ 搭测评机是一种很好的尝试,自己两个单元的测试都是采用自动化测试的方式,第二单元主要通过自动化测试来选择好的调度策略,在 hack 上没有第一单元那么有效。

测评机的搭建步骤主要分为:

  1. 测试数据的生成(最简单的部分);
    • 主要通过 python 来自动生成
  2. 将测试数据导入到测试代码中;
    • 这次需要按照时间戳,定点爆破,使用了 python 中的 subprocess.Popen,将输入数据时间戳进行简单正则处理,再投放
  3. 将测试代码生成结果导入待检查文件中;
    • 使用命令行的文件拼接,检查的时候需要对输入和输出两者进行对比
  4. 检查输出结果。
    • 使用 java 生成 jar 包来进行检查,,可以通过命令行直接调用,设计十分面向对象,同时也能帮你很好理清楚可能出现的潜在问题(感谢wyk大佬在检查输出上的指导)

我一般通过 shell 脚本来编写自动化测试,因为调用逻辑就和命令行操作一样,再通过输入输出的重定向就能很好地完成评测。

​ 选择调度策略的时候自己写一个爬虫,爬自己每种调度策略跑相同输入的结果,然后进行简单平均值,加权排序,选出最优策略(同时我也用这种方法找到了屋中最强大佬的代码进行观摩,受益匪浅)。

四、多线程学习小结

  • 多线程的作业我认为首先是正确性,即线程的安全性与调度的合理性,其次才是调度策略上的优化等。
  • 有部分经验来自于近期的 OS 课程,关于怎样合理分配作业给 CPU,我第六次作业的最少任务电梯优先的策略就可以类比作业分配中的短作业优先,我的调度策略主要是注重任务分配的均衡方面
  • 多线程的框架在线程调度中有重要地位,例如这次的生产者-消费者模型,将共享资源抽象出来成为一个类,方便我们更为直观地进行代码编写和维护

五、心得体会

  • 我感觉面向对象的学习不像是学习任务的大头,反而在测试程序,优化上需要花费更多的心思,但是写面向代码和编写测试程序两者又是相辅相成的,尤其是在写检查程序的时候,会让自己关注到更多的细节和可以优化的地方
  • 在初始的时候为自己的代码留下扩展性,在代码的框架上有了点迭代的味道,难得的一次舒服的一单元的 OO 课程
  • 框架和性能是相辅相成的,好的框架的设计和有条理的调度逻辑能够保证你的性能不差,具体的优化只要落地到细节上,而不是大改框架去实现所谓的”优化“
  • 多点尝试,尝试不同的调度策略,尝试不同的换乘分配
  • 多看看设计模式,积极地应用或许有意想不到的效果

猜你喜欢

转载自www.cnblogs.com/DanGuge/p/12709218.html
今日推荐