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

一、概述

这一部分的作业考察的关注点与上一次的作业有所不同,上一次的考察重点主要集中在输入输出的判定以及多态的考察上面,而这一次是让我们进行多线程程序的调度与开发。这次开发过程中最大的感受就是自己之前的程序好像都白写了。。。需要自己去探索掌握的东西有许多。在之前的作业中我们都是尽可能地使用加断点的方式进行调试,因为每次程序运行不会产生二义性,即对就是对错就是错,这时printlog就成为了十分有效的debug方式。同时这一系列的作业是对上一次多态思想的继承,即第一次作业在设计的时候就需要弄清楚可能增加需求的地方,提前预留出可能的扩展接口。在扩展的时候如何不重构之前的代码而是选择在之前的代码上进行协同扩展。这两大块应该是笔者在这轮作业中学到最多的东西

二、电梯之一--单部FAFS电梯

1. 需求:设计一部多线程傻瓜调度(先来先服务,电梯中可只有一个人)电梯。

2. UML类图

3. 时序图

4. 类与方法的度量分析:

 

由于第一次作业较简单,代码量较少,故复杂度在可控范围内。

5.设计策略

本次作业仅仅只有一部电梯,而且调度的方式也是十分简单的FAFS,电梯中仅需满足有一个人即可。这样的形式让我直接选择了只有两个线程--处理输入的InputHandler(AddToLiftQueue)以及处理进行电梯运行的ElevatorScheduler(ScheduleLiftQueue),Elevator类只提供电梯运行状态以及方法(上下开关门)供Scheduler调用和查看。

InputHandler会将Request解析为新的LiftRequest(只是把id,from,to等信息分开记录,然后加上该指令是否被运行的状态),然后送入共享队列供Scheduler调用。第一次我是用的是java的阻塞队列,这样也就是简单的put与take就结束了,所以第一次并没有特别具体地进行并发处理的研究,因为BlockingQueue具有队列为空时消费者暂停,队列满时生产者暂停以及当非空或非满时通知消费者与生产者继续运行的功能。这也为第二次作业的修改带来了隐患。

在线程退出的方面我采用了在input发现没有输入的时候立即产生一个id为-1的队列结束请求,然后该线程退出,之后传播到scheduler后scheduler在处理完所有其他请求后也会捕获这个信息,退出线程。

-- 优点:

  · 将电梯的请求进行了重新分析,将同时将状态信息标记在每一个请求内部便于后续查看。

  · 请求输入与电梯调度分离,做到了多线程处理,结构较为清晰。

-- 缺点:

  · 类命名时没有以input,scheduler与elevator这样的大类区分,而是面向对queue操作进行的区分(addtoqueue,schedulequeue),没有将scheduler与elevator分离为两个线程。

6.设计原则检查:

· S:由于代码较少,基本所有类的所有方法都只进行一项功能执行。

· O:没有支持scheduler与elevator的线程拆分,后续实现需要重构,没有对开闭原则考虑清楚。

· L:没有子类,满足L要求。

 · I:没有接口,满足I要求。

· D:没有明确的类间依赖。

三、电梯之二--单部ALS电梯

1. 需求:设计一部多线程可捎带乘客调度电梯。

2. UML类图

3. 时序图

4. 类与方法的度量分析:

 

Elevator类的复杂度较高,内含许多控制性逻辑。

圈复杂度较高的几个两个方法均是Elevator.run()中调用的主要过程方法,内嵌许多细小方法,控制逻辑较多,复杂度较高。

Scheduler的run()由于存在check电梯属性与运行方向等特征,选择判断较多,故圈复杂度也较高。

5.设计策略

本次作业对电梯的调度策略进行了要求,即需要满足乘客实时的捎带请求,否则就会run time exceed。由于电梯要求进行可捎带的指令,所以按照原来的顺序进行电梯请求处理的这种方法并不可取,所以必须要将scheduler和elevator分成两个线程分别进行处理。

笔者在本次作业中设计了3个线程,InputHandler线程仍然执行接收请求并进行记录的解析,传递给Scheduler,这里将scheduler设计为将一段时间内的input传来的请求进行分发,然后再传给elevator线程的request队列这样一个线程。elevator线程自己处理自己的运行方向并根据当前的楼层进行捎带。

在线程同步上,由于笔者认为阻塞队列只能对队列头部和尾部进行操作,所以打算采用wait和notify的模式进行。笔者将input队列和schedule队列进行了分离。InputHandler和Scheduler队列共享input队列,Scheduler队列和Elevator队列共享电梯运行状态和schedule队列,笔者这么做的初衷是想让暴露给电梯的队列都是可以进行捎带的,进而方便电梯的请求存取,但是在Scheduler和Elevator之间进行电梯状态的暴露使得我的设计逻辑十分混乱,其实后来想一想电梯的状态信息和电梯选择捎带指令应该放在一个线程中减少锁的使用。由于scheduler向schedule队列放入request时一定会向schedule队列加锁,而elevator线程几乎时刻都在检查elevator的队列以及电梯状态,最后几乎就是elevator运行时一直攥着schedule队列,scheduler队列几乎成了elevator线程执行时的线程,即只在elevator需要更新捎带队列的时候才一定会释放schedule队列的锁。这样就造成了elevator逻辑耦合性极高而且一直占据着队列锁,而scheduler几乎和elevator线程串行工作。

-- 优点:

  · 设置了电梯自己的等待队列,便于向后续多电梯进行扩展。

  · 将所有楼层上下,电梯开关操作封装成方法,电梯状态包装为属性类简化电梯运行逻辑。

  · 请求中加入属性类标志请求是已经在电梯中还是等待可捎带等待电梯取用还是已经被执行完毕抛弃。

-- 缺点:

  · Scheduler和Elevator共享的情况除了共享队列还有电梯状态,造成线程间交互极多,影响扩展性。

  · Elevator类的逻辑过于庞大

6.设计原则检查:

· S:InputHandler和Scheduler较为简单,而电梯内方法较多且较为交织,存在很多方法调用嵌套。

· O:对电梯的属性,楼层的运行实现了建模,但是调度器的框架搭建得不完善,存在很多主要还是类与类间的依赖没有拆分清除。

· L:没有子类,满足L要求。

 · I:实现了一个调度器获取电梯状态的接口,但实际上并没有使用,只是直接请求的电梯状态。

· D:电梯内部存在较多依赖关系,可以将楼层等信息进一步封装成接口。

四、电梯之三--多部智能调度电梯

1. 需求:设计多部多线程智能调度电梯。

2. UML类图

3. 时序图


注:只画了一部电梯,剩下两部电梯相同。

4. 类与方法的度量分析:

 

可以看出Elevator类仍然一如既往地臃肿。。。

OriginalRequest类为分割请求的总类,下有三级结构,每层需要定义,故复杂度较高。

方法中复杂度较高的与类相一致,可能就是他们较高的复杂度导致类的复杂度较高。

5.设计策略

第三次作业需要处理多部电梯,同时需要处理电梯指令的拆分与同步,因为一个指令不能从指令开始层数直达指令结束层。

基于此需求,笔者保留了InputHandler的处理输入功能,同时扩展了InputHandler里面的指令解析功能,将指令拆为3级。第一级为OriginalReq,数量与原有请求数量相同;第二级为SplitReq,这一级是所有OriginalReq可被拆分的方式,每一个splitReq对应一种不同的换乘情况。第三级就是LiftReq每一种换乘情况的第一部分起始点终点以及第二部分起始点和终点。笔者观察了所有的不能直达情况,发现一次换乘就可以满足所有需求,故在此只要求换乘一次,并且换乘两段方向必须相同以约束换乘情况。这部分工作都交给InputHandler来做。

接下来是TopScheduler进行指令的分发,TopScheduler会记录历史各地换乘情况,选取所有换乘点中较少换乘次数的楼层进行换乘。同时给换成两段进行同步,即设置mutex变量防止后一段先于前一段执行的情况。之后就选取每一种originalReq的最优splitReq进行分发,将splitReq中两段LiftReq分给对应的电梯调度器。

电梯的调度器是用来分离可调度队列的。即给电梯一个不需要与TopScheduler进行交互的队列。

电梯负责在每层选取可调度的请求,采取扫描算法,即在同方向未到达楼层的所有该方向的请求都捎带之后才进行方向的切换。

协作性在于InputHandler有请求后对TopScheduler进行唤醒,TopSheduler非空进而唤醒ElevatorScheduler,最后ElevatorScheduler唤醒Elevator。这次对队列采用了第一次电梯的阻塞队列方法。只是对阻塞队列进行了遍历操作的保护,即每次遍历都需要获得待遍历对象的锁,若有多次遍历则多次遍历结束后才释放锁,这样保证了遍历时的线程间同步。

-- 优点:

  · 相比前两次架构清晰了许多,InputHandler,两级Scheduler以及电梯运行功能明确

  · 采用扫描算法进行调度,效率比第二次的主请求高很多。

  · 将Scheduler与Elevator的队列实现分离,防止Scheduler队列检查电梯状态

-- 缺点:

  · 电梯逻辑仍然很臃肿。应该尝试封装上下楼层以及开关门的类。

6.设计原则检查:

· S:除了Elevator类,其余类逻辑和方法紧凑而专一。

· O:由于第二次的设计失误,这次再Scheduler和Elevator之间的联系改动较大,只能选择重构,故此要求未满足。

· L:TopScheduler和Elevatorsheduler类都继承自Scheduler类,主类实现了队列的吸入和拿出。

 · I:没有接口,满足I要求。

· D:电梯中间仍然有较臃肿逻辑。

五、BUG发现

虽然在设计上并不是特别完美,但是由于笔者通过工程化方法进行了debug,最后的公测强测三次均没有被发现出bug,说明程序在逻辑上还是经得起推敲的。由于笔者是高工学生,故没有互测部分,这部分就简要略过,不过后续的bug再次筛查还是会进行下去。

六、心得体会

本次作业的一个最核心的思想就是如何最大化利用多线程来解决问题而不会产出不可预料的bug。在本次作业中笔者首先体会到了java.Concurrency封装后的强大,这种封装(例如阻塞队列)给特定的需求(生产者消费者)提供了极大便利。但是笔者认为至少初学者要自己完完整整实现一下这种功能才会完全地掌握这种功能的使用,否则使用后可能不知道bug的发生地而陷入漫长的debug过程。其次,笔者在这个过程中学到的就是架构设计的重要性。在第二次的电梯作业中,笔者没有对线程之间的“同步交付”理解到位,在sheduler和elevator加了太多的锁进而导致某一个sleep被锁在了一个对象中,造成其他线程无法被唤醒。后来才采用了每到一层统一交付的方法进行代码编写。

另一个很重要的SOLID思想也是本次编程中体会很深的一个,这也是编程架构的一个评判标准。如何在初期预料到可以进行扩展的地方并进行相应设计是一种很重要的能力。在如何从第一次作业到第二次作业中的重构吸取教训从而使得第三次作业减少重构而多进行扩展是一种更重要的能力。在经历了第二次作业的大改后,笔者清楚地认识到设计保持SOLID不仅需要只是抽象层次上的关系确定,更是落实到每一个方法,每一个接口的设计与转换上。在下一轮作业笔者会继续探索SOLID的思想,争取在前两次作业设计出老师所说的后一次作业只需要改50行代码即可的架构!

猜你喜欢

转载自www.cnblogs.com/pianomposer/p/10758998.html