OO第二单元多线程电梯总结

OO第二单元总结

一、前言

第二单元完成了三次可稍带多线程电梯的设计,功能需求逐次扩展。经历了第一单元三次作业三次重构的我,终于在第二单元的三次作业中体会到了迭代开发的快乐(不用重构每周代码量少了好多)。纵观三次作业,第一次作业的架构的选择起到了相当重要的作用。同时也经历了随着需求的提升,代码的耦合度也随之上升,可扩展性剧烈下降的过程。此外,第一次接触多线程并发程序设计,使我把三次作业设计的目标都放在了减小线程间的耦合上,尽量使工作在线程内完成,这样的设计也与面向对象的思想相吻合。但是一些优化又需要在保证线程安全的情况下,增加线程间的通信,所以放弃了部分想到的优化,正如本周的理论课上讲的,功能设计与性能设计需要找到一种平衡。

二、作业程序分析

1.第一次作业

内容:单部多线程可稍带电梯

设计思路:

设计三个线程:输入线程,调度器线程以及电梯线程,这三个线程之间满足两个消费者生产者关系。在输入线程与调度器线程之间,使用一个主要属性为CopyOnWriteArrayList的类作为共享队列。输入线程每接收到一个输入就将其传入共享队列,调度器作为消费者查询共享队列获取请求。电梯线程与调度器线程间有一个共享队列。这个队列是由每个楼层向上向下的队列组成,每个楼层和方向对应的队列仍然是CopyOnWriteArrayList的封装类。整体封装类拥有根据楼层以及方向获取下标的方法,调度器根据请求的楼层和方向将传入的请求放到对应楼层方向的队列中。

对于电梯的调度方法,有一说一,我不是很理解可捎带的理念,觉得ALS是一种实现复杂,耦合度高并且效率低的运行方法。经过在网上的调查,最终确定了LOOK的运行算法。即为:在内部外部请求都不为空时,电梯保持上下运动,目的楼层与运动方向符合的请求都可以进入电梯,电梯变更运动方向当且仅当目前运动方向前方的内外请求均为空。LOOK算法的实现也相对简单,所有分配上下的操作都由电梯类完成,提高了功能的内聚性。

线程的终止阶段,使用了一个有风险产生死锁的方法:在各个线程之间再建立一个管理线程停止的共享变量,相当于线程间交互拥有了两种不同的锁。很遗憾这个错误在第二次作业的互测中才被发现,类似的错误也导致第二次实验出了问题。

线程安全与死锁,不愧为影响多线程设计大魔王,更加奇幻的是这些bug具有经常隐身偶尔出现的功能,bug能否被测出来看脸,能否被复现更看脸。非洲人民只能寄希望设计出完全不会出现线程死锁的程序。

度量分析:

OO第二单元多线程电梯总结_第1张图片
OO第二单元多线程电梯总结_第2张图片

复杂度比较高的方法包括:电梯有关运行,查询的方法,分配器分配的方法,由于电梯与调度器的run方法的很多功能都被提取成单独的私有方法,这些方法的依赖程度相对也较高。

队列查询是否为空涉及到遍历相关,因此循环复杂度比较高。

UML类图:

OO第二单元多线程电梯总结_第3张图片

主类创建线程以及队列对象,STAT对象提高了线程的耦合,建议删除。

UML时序图:

OO第二单元多线程电梯总结_第4张图片

2、第二次作业

内容:多部可稍带多线程电梯的设计

设计思路:

在第一次作业的基础上,只做了几个小的改变。首先,对于每个电梯线程,添加max_vol属性,保证每个电梯线程内部人数小于人数限制。其次,对于多个电梯,一个电梯拥有自己的ElevatorQueue队列,每个电梯的队列放入一个容器存入调度器。分配方式使用平均分配,即一个轮次变量round,依次对应电梯的队列在容器的下标,获取电梯后加入对应队列,round自增,超过电梯数量之后返回至指向第一个电梯队列。

这样的设计显然不是最优解,但是在保证电梯与调度器耦合尽量小的情况下,并非不是一个可行的方案,同时也避免了电梯竞争导致的资源浪费。

整体架构与类的具体方法都与第一次作业基本相同,在此不做赘述。

3、第三次作业

内容:支持动态加入和换乘的多部可捎带多线程电梯的设计

设计思路:

相对于第二次作业的功能有两个方面的拓展:对于电梯,限制了可停靠楼层,不同类型的电梯容量及运行时间也不同,部分请求需要换乘。同时,输入线程可以动态加入电梯。

对于动态加入电梯,由于电梯上限为6台,我使用了一个比较偷鸡的方法:建立足够的电梯线程,在其对应的队列中加入valid属性,前三台ABC初始valid为true,之后加入三种类型分别三台valid为false的电梯。valid为false的队列不会获取请求,电梯一直处于sleep的状态,几乎不占用资源。调度器获取添加电梯指令之后只需要将其ID输入至队列并且置valid属性为true,就可以获取请求了。

由于换乘请求的存在,将PersonRequest封装为person类,调度器在获取请求之后,实例化一个person对象,person类中有是否换乘以及在换乘的第几程的属性,在构造器中判断请求是否可达,若可达,封装PersonRequest对象的方法即可。若不可达,分析其换乘楼层,封装方法根据当前行程位置输出,并且初始化相应属性。而调度器根据person类可达的电梯类型,在对应电梯类型容器中平均分配请求。

电梯队列与电梯获取的都是person类,电梯也根据person类的封装方法完成接送。由于提前规划换乘路径,电梯在换乘请求第一程结束后可以直接将其加入另一电梯队列,继续下一程的换乘运行。

在电梯队列与输入队列中加入有关停止的属性,使电梯调度器输入间的所有操作都在一个托盘内完成,减小死锁不能停止的可能性。

度量分析

方法复杂度:

OO第二单元多线程电梯总结_第5张图片

OO第二单元多线程电梯总结_第6张图片

OO第二单元多线程电梯总结_第7张图片

类复杂度:

OO第二单元多线程电梯总结_第8张图片

和第一次作业相比,本次作业的方法复杂度和类复杂度都极大的增加。首先是调度器,内部功能过于冗杂,拥有的容器数组包括:A,B,C类电梯集合,A,B,C类电梯可达楼层数组,所有电梯集合,每个电梯的楼层集合,输入请求集合。特别是电梯可达楼层数组需要经常遍历,寻找电梯的下标的方法有高达12个分支条件。很明显,这里的调度器需要分层设计来保持正常的复杂度了。此外,电梯线程队列与person类都需要经常遍历电梯可达楼层集合数组,循环复杂度也达到了一个很高的水平。

从度量分析来看,第三次作业的代码复用性降低了很多,许多功能冗杂,出现了planner这种巨型类,电梯类之间的耦合也很大。很明显的看出为了实现复杂的功能,清晰的结构已经变得模糊混杂了。

UML类图:

OO第二单元多线程电梯总结_第9张图片

仍然是经典的三条线程,两个共享对象的生产者-消费者结构。

UML时序图:

sequence2OO第二单元多线程电梯总结_第10张图片

三、第三次作业分析

基于SOLID的程序评价

单一责任原则(SRP):

整体的架构基本满足单一责任原则,输入线程控制输入处理,调度器线程控制请求能够分配到每个电梯每层的队列中,电梯负责读取队列满足请求。本次作业的设计在这一原则下存在两个问题:

  1. 控制器的功能过多,可以向下细分
  2. 电梯负责加入换乘请求的队列,应该单独建立一个换乘处理类完成
开闭原则(OCP):

电梯线程的设计基本满足新增其他电梯不需要修改代码的需求,但控制器线程使用了大量的 final 的数组,不利于拓展与修改,但是前两次作业的调度器扩展性较强。

可替换原则(LSP):

本次作业没有使用继承,电梯类可以设置为继承的模式。

接口分离(ISP):

本次作业没有需要设置接口来实现层次化设计的需求。

依赖反转原则(DIP):

缺乏层次性结构,每个类的功能层次均相同。

整体分析:

本次设计更加偏向于相同层次的类的协同,没有重视抽象与具体的层次结构,导致一些类在面向不同需求的时候需要部分重构。特别是第三次作业,由于没有下一次作业迭代,设计时也缺少从扩展性上考虑。

四、bug分析与测试方法

本单元作业在公测与强测中都没有测出bug,但是第二次作业在互测时因为无法停止被hack了,经过分析是结束机制可能产生死锁的问题,即两个电梯分别获取电梯队列的锁和结束对象的锁,又分别等待对方的锁。在开发过程中,出现的bug主要有:无法接到反方向的需求使电梯在两层楼来回运行,换乘的可能提前结束,电梯无法唤醒等,主要来源于电梯运行策略线程安全问题

测试他人的代码,在第五次互测测出一次死锁问题,第七次互测测出当添加电梯为最后指令时无法退出的问题。主要的问题还是在线程运行上。

测试方面,主要以自动构造测试数据+手动功能覆盖测试为主。但是由于python等评测机语言学术不精,没有构造出较好的模拟输入的环境。通过重写输入接口,能够仿造实现延时输入,但是效果很一般,时间误差极大。第七次作业白嫖一套评测程序,成功在公测阶段发现几个自己的bug。

五、心得体会

反正没人敢坐我设计的电梯。

通过三周的电梯作业,我第一次经历了多线程程序的开发过程,能够顺利完成,很庆幸在第五次作业开工之前思考了一晚上架构和调度方法。如果直接实现最开始的奇怪想法,可能又要不知道重构多少次了。但是,成熟的架构也需要成熟的实现方法,自认为前两次作业的实现方法是简单而清晰的,第三次作业因为没有拓展的需求或多或少有点放开了写的感觉。

线程安全玄之又玄

多线程相关的玄学问题,自己的原则是能避开就避开,线程间的通信能减少就减少,尽全力避免出现需要看脸来复现的bug,所以后两次作业在性能优化上面做的还不够,一些能做的优化都没有实现。不过也学习到了通过print大法和jProfiler检查线程的综合调试方法。

OO的思想继续渗透

你一个main函数能把我多线程电梯秒了,我当场把这个电脑屏幕吃掉。

对于面向对象的思想,每个对象负责自己功能的SRP原则已经牢记在心,但是有关抽象继承实现的问题,还需要进一步的打磨。

无论怎样,和一个月之前相比,再也不是三次重构,设计首先想的再也不是一main到底,进步总是令人开心的。同时,能够不吃人不失踪不瞬移把纸片人送到家,也是有一点点成就感的。

你可能感兴趣的:(OO第二单元多线程电梯总结)