BUAA-OO2022-UNIT2总结
一、前言
本单元主要是通过电梯系统来训练多线程的相关应用。个人认为在难度跨度上没有第一单元那么大,但是debug的难度大大增加,对于加锁部分也更需要逻辑清晰、结构合理。本人第一次借鉴实验课的架构,把个人认为多余的调度器删除,之后在架构上都没有太大的改动,主要是增加各种“补丁“。但是综合来看思路还是不是很清晰,特别是在涉及加锁的部分,我的设计思路导致不少非设计性bug难以检查出。
二、一些重要的知识
- 
线程状态 ![]() 
 ![]() 
- 
加锁方式 - synchronized:同步修饰符——尽量用同步块!
- Lock对象——通过显式定义同步锁对象来实现同步。
 
- 
生产者消费者模式 - 场景:
- 假设仓库只能存一件产品,生产者将生产出的产品放入仓库。消费者取走消费
- 若仓库无产品,则生产者生产,此时消费者等待,直到有产品生产出来
- 若有产品,则生产者等待,直到消费者消费后又开始生产
- 两条线程需要互相“通信”
 
- 实现方式
- 管程法(本人选用)——设计缓冲队列
- 信号灯法
 
- 重要方法
- wait(): 线程进入等待状态,直到其他线程通知,与sleep不同,会释放锁; 通常需要用while包裹而不是用if包裹
- notifyAll():唤醒同一个对象上所有调用wait方法的线程,优先级高的先调度。
 
 
- 场景:
三、历次作业分析
第5次作业
- 
基本架构: ![]() - 总共两类线程——输入线程(InputThread类);每个电梯有一个运行线程(Process类)
- 生产者消费者关系
- 生产者:InputThread。接收输入信息并将其存入缓冲区——PersonTable类
- 消费者:Process。有一个Elevator的对象,体现该运动过程是发生在谁身上。Elevator中存放电梯的各种信息以及不同基本行为(包括从PersonTable中搜寻主请求、上下乘客等方法)。Process对象在run()函数中用while根据电梯的不同状态调用电梯的不同行为。
- 缓冲区——PersonTable。单例模式,其中包括五个数组(每个由10个RequesQueue对象构成),用来存放五座楼的候乘请求信息。
 
 
- 
同步块与锁 该次作业中因为第一次接触多线程,锁其实加得不是很清晰,根据加锁的对象总共可分为3类。 - RequesrQueue类:中所有方法都加上了synchronized修饰符以及notifyAll()函数,当时是因为沿用了实验中的代码,但后来分析其实没必要把锁的粒度设定这么细。
- PersonTable类:PersonTable类中的setEnd()和isEmpty()方法加上synchronized修饰符,因为此处需要对多个RequesrQueue对象进行原子性操作
- PersonTable类中某一座楼的候乘请求数组(RequestQueue[]):
- InputThread中加入请求时要对对应楼层的请求队列数组加锁,并且notifyAll();
- InputThread结束时要对PersonTable进行setEnd()操作,此时需要把所有候乘请求数组唤醒。
- Elevator类寻找主请求时需要遍历对应座的请求数组,要对请求信息数组加锁;当找不到且不该结束线程时需要在请求数组对象上调用wait()(本次唯一的wait).
 
 
- 
调度器设计 如前言提到,当时个人认为调度器没有实际功能,知识在生产者和消费者之间加了一层,并且在设计setEnd的时候,这样多的一层导致之间的逻辑更不清晰了,故最终删除了调度器。 由InputThread直接把所有请求按座和楼层加入PersonTable的对应位置。 Elevator确定主请求时自行在电梯内部找或者遍历该座的请求信息数组,找到“最优方案”。 
- 
bug分析 - 
轮询 当时困扰了我很久,主要就是最初有调度器但调度的方式又不是很合理,一味沿用实验中的代码,如果Schedule类不wait的话一定会出现轮询。 此外,当时由Schedule类来判断PeronTable是否最终结束了,会导致电梯进程不断while循环判断是不是需要结束线程。 最终将调度器删除后变清晰了一些,解决了轮询。 
- 
输出线程不安全,第一次没有注意到 
 
- 
第6次作业
- 需求变更:增加了横向电梯,并且同一座、同一层允许多个电梯
- 
基本架构: ![]() - 在PersonTable中新建了十个数组用来存放横向电梯每一层的请求信息
- 为了实现横向电梯直接新建了一个ProcessHorizontal类和ElevatorHorizontal类,基本思路其实与第一次类似,只不过其中一些设计楼座的地方改成楼层,计算方向和遍历请求信息数组时进行微调
- 总共两类线程——输入线程(InputThread类);每个电梯有一个运行线程(Process类)
- 生产者消费者关系与第一次相同
 
- 
同步块与锁 该次作业在设计上几乎是把第一次作业中的纵向电梯复制到横向电梯,加锁的方式没有改变中。 
- 
调度器设计 也与第一次一样没有专门的调度器。采用简单的生产者消费者模式 
- 
bug分析 第一次设计中存在一些不合理之处,这次在一座多电梯的情况下得以体现,主要是会发生两个电梯同时争抢一个请求、一个请求可能上两个电梯的情况。 针对此设计在Request类中设计了一个selected属性,当其被选作某一电梯的主请求后即将其置1,其他电梯不能选。同时在isEmpty等相关判断也增添该属性的判断。 
第7次作业
- 需求变更:可以换乘、限定横向电梯的开关门
- 
基本架构: ![]() - 在InputThread阶段就把每个请求的换乘楼层设定好(若需要换乘)并作为一个属性记录在Request中
- 在电梯的getOff函数中新增加入到横向电梯的判断,若需要换乘,则将其加入横向地电梯的对应请求数组
- 让两种电梯都实现Elevator接口,实现工厂模式,在personTable中记录当前所有的电梯——用于最后判断何时结束线程。
- 总共两类线程——输入线程(InputThread类);每个电梯有一个运行线程(Process类)
- 生产者消费者关系与第一次相同
- 对于分段操作没有引入标准的流水线模式,而是简单得采用第一阶段完成后直接把请求再加入新的请求队列的模式。但似乎可扩展性没有采用标准流水线模式强。
 
- 
同步块与锁 该次作业更改了第二次作业中对某一座的请求信息数组加锁的模式,因为此时某一个线程需要wait时需要判断所有电梯是否都空且所有请求数组都为空,所以调整为全部对personTable对象加锁。虽然看似能同步执行的操作变少,效率应该降低,但从结果上看运行时间反而有变快,个人分析原来的加锁粒度太细,线程切换过于频繁,反而耗时。 
- 
调度器设计 也与第一次一样没有专门的调度器。采用简单的生产者消费者模式 
- 
bug分析 - 最初在getOff函数之后简单加上添加到新请求队列的代码,形成先从电梯队列中remove再add到新请求队列的逻辑顺序,若在这两者中间要换乘的线程判断是否结束线程,则电梯和请求队列可能都为空,从而提前结束。修改方法只需要先加入新请求队列在从当前电梯队列中remove即可。
 
- 
复杂度度量 - 
缩写含义 - 
OC:类的非抽象方法圈复杂度,继承类不计入 
- 
WMC:类的总圈复杂度 
- 
ev(G):非抽象方法的基本复杂度,用以衡量一个方法的控制流结构缺陷,范围是 [1, v(G)] 
- 
iv(G):方法的设计复杂度,用以衡量方法控制流与其他方法之间的耦合程度,范围是 [1, v(G)] 断。(G):非抽象方法的圈复杂度,用以衡量每个方法中不同执行路径的数量 
 
- 
- 
类度量 ![]() 
- 
方法度量 
 ![]() 
 ![]() 
 ![]() 
 ![]() 
- 
分析 - 类的总圈复杂度过高了,说明设计上耦合度高,扩展性弱。确实,这单元由于时间紧,许多推荐的设计模式都没有使用,最后只以最简单的生产者消费者模式为基础然后不断加补丁,导致代码性能下降,需要在以后注意改正。
- 可以看出两个Process类的run方法都复杂度过高。确实,这两个方法当时为了简便一直扩充,最终超过了60行,虽然能够实现正确性,但在可读性和可扩展性上欠缺,耦合度太高。
- InputThread中选择换乘楼层的方法也复杂度较高,设计了多个类,在一个函数中综合考虑了几种特征的综合情况,如果有调度器的话这部分应该能更清晰。
 
 
- 
四、心得体会
这一单元的作业是对多线程思想和应用的练习。本单元作业总体写得缺乏“美感”,最终虽然能实现正确性,但在各种设计模式和思想上其实没有练习到位,以后需要好好分析架构再进行代码编写。
本次在评测方面也不足,因为时间关系,大多只能采用肉眼干瞪法来找bug,作为计算机专业度学生需要有强自学能力,之后需要尽快掌握命令行、shell、python、一些调试辅助包的相关内容并能够应用。
 
                    
                










 
                
            
         浙公网安备 33010602011771号
浙公网安备 33010602011771号