一、多线程的协同和同步控制方面的设计策略
得益于第一次作业架构设计的足够强大并且留下了充分的接口,后续两次作业基本只是小调整加优化调度策略而已,所以以下三次作业均不做区分叙述,三次作业线程间均采用了异步通信的设计策略,好处在于没有共享资源,避开了锁与同步控制的麻烦,数据拷贝带来的时间空间浪费对于这种小规模仿真问题来说也可以忽略不计,主要困难在于异步导致的数据不一致。考虑电梯状态改变->调度器收到状态改变消息->调度器进行调度->电梯收到调整指令,这样一个过程,由于采用非阻塞的异步的通信设计思想,不对线程的运行顺序做任何控制与假设,电梯在发出状态改变的消息之后就接着运行了,调度器无法保证在调度的时候,拥有最新的数据,而且到电梯处理上一次调度发来的指令的时候,状态可能又变了,因此调度器,尤其是电梯,在处理收到的消息的时候,采取了一种不信任的设计思想,它首先验证该消息是否适用于当前情况,并对不同情况作出对应处理,进而解决了这些问题,控制器线程就能够与电梯线程很好地协同工作了。
二、功能设计与性能设计的平衡、可扩展性、设计原则对第三次作业的分析
a) 功能设计与性能设计的平衡
为了保证线程安全,线程间采用了消息队列、异步通信、无共享变量的模式,强隔离性使得调度器并不能实时了解电梯情况,调度器通过指令指挥电梯,且指令必须要有一定的容错度,这限制了调度的性能,较为极限的调度扩展是无法实现的,一些trick就更不能做了,即为了功能设计牺牲了一部分性能设计。但同时,通过对调度器采用Strategy策略拆分并采用层级调度的模式,保证了调度策略有很大的可扩展性,并且通过对调度策略的改进,最终性能分还不错。经过两次迭代,一开始留下的接口基本都已经用上了,直接使用原有的接口扩展空间不大了,但对于请求类、调度类、节点类都做了抽象处理,可以容易做到通过新增代码而不是修改代码来扩展功能。
b) SOLID分析
i. SRP原则
- Controller类的职责是基本单一化的,它负责监测状态改变,记录请求,调用CenterScheduler类得到调度结果并发送,但是独自检测程序结束这个有些复杂,应当考虑分派一部分到Elevator类协作完成。
- Request的实现类的职责都是单一化的,它只负责传达信息或指令
- Tray类的职责是单一化的,它只负责转发消息
- Elevator类的职责是复杂的,它要独自维护电梯的状态,接收并完成来自Controller的命令,实现电梯的运动,开关门,进出人,并且要注意检查自己是否需要将状态汇报给Controller,这个类及其中的一些方法是略显臃肿的,违反了SRP原则,事实上在第二次作业到第三次作业的迭代中已经对Elevator类做了小重构,提取出了部分功能形成新的类并采用组合模式协作,但是由于懒没有做到底,如果还有后续的迭代,是应该考虑拆分Elevator类的
- Passenger类的职责是单一化的,它只负责按路径到达目标楼层
- Middle类的职责是单一化的,它只负责计算最佳路径
- MainClass的职责是单一化的,它只负责处理输入
- CenterScheduler的子类职责是单一化的,它只负责控制各个Scheduler调度的顺序
- Scheduler的子类的职责是单一化的,它只负责根据一部电梯和楼层状况给出指令
- 还有一些主要是存放信息的类,也是符合SRP原则的
ii.OCP原则
采用Strategy模式,设置了CenterScheduler和Scheduler两级抽象类,通过继承并在子类中实现调度策略,使得调度器符合OCP原则,可以很容易通过新建子类扩展策略。建立MyNode,RimNode,CenterNode接口,并在Tray类中提供了为Node转发消息的服务,使得增添新线程是符合OCP原则的,只需要实现RimNode/CenterNode接口即可接入消息网络。
iii.LSP原则
子类均没有改变父类的功能,符合LSP原则
iv.ISP原则
接口的内容都充分地专一化,符合ISP原则
v. DIP原则
Controller是依赖于CenterScheduler抽象类而不是其具体实现的,符合DIP原则,但是为了利用PriorityCenterScheduler中的电梯优先级信息更智能的设计乘客路径,Controller单独识别了PriorityCenterScheduler并特别处理了,在一定程度上违反了DIP原则。CenterScheduler是完全依赖于Scheduler抽象类的,符合DIP原则。
三、基于度量来分析程序结构,类图及协作图
从方法复杂度度量来看,整体的复杂性不算很高,平均值相对上一次作业有显著改善。
从类复杂度度量来看,职责复杂的Elevator类和Controller类总复杂度显著性高,由于内部方法的拆分,OCavg还行,Middle类的复杂度可能与初始化打表的代码套了5层循环暴力搜索路径有关,可以不考虑,后续应该考虑拆分Elevator类和Controller类的职责。
从类图中看,对继承的和实现的使用不是很多,层次也比较浅,对于线程间通信采用消息传递并加了中间转发层的结构,因此建立了MyNode接口,支持receive方法,并被Tray类管理,以及RimNode,CenterNode两个接口,前者支持registerTo方法,将自己登记到一个CenterNode并接受登记的回复,后者支持register方法,接受登记请求并处理。没有展开画出的request包中定义了一系列的请求类,具体内容在协作图中有很好的体现。
CetnerScheduler抽象类负责调度顺序,有四个实现类,最终实际被调用的是PriorityCenterScheduler,采用的算法是计算电梯种类的一个动态的优先级,当某次调度得出一台电梯该空闲的时候,提高其种类的优先级以试图均衡负载,种类优先级高、空位数多的电梯将先得到调度权。
Schedulere抽象类负责具体的调度,有两个实现类,最终实际被调用的是LookScheduler,采用LOOK算法实现。
通过这种抽象层次,Controller依赖于CenterScheduler,CenterScheduler依赖于Scheduler,保证了调度策略的易扩展性。
再看协作图,程序总共有若干个Elevator线程,一个Controller线程,一个Main线程,一个Tray线程组成。
Main线程处理输入并以异步通信的方式告知Controller,Controller维护各个Passenger所在楼层的表,以及记录一张电梯状态表,在每次收到消息时处理,并以同步调用的方式更新Passenger类中记录的下一个目标层信息,而Passenger类实际上通过调用Middler类提供的算法得出自己中间目标层并更新,随后Controller以同步调用的方式利用自己的组件CenterScheduler得到调度结果,并视情况以异步通信的方式通知各个Elevator。Elevator类能妥善处理Controller的命令,并自动完成运行。
四、分析自己程序的Bug
在强测、互测中没有出现Bug,有一些在开发过程中出现的Bug值得注意,例如在第一次作业的设计过程中,出现过所有线程都陷入阻塞无法唤醒的死锁状态,其原因在于电梯本应当对每一个进入人员请求给出回复,以唤醒调度器正确处理人员,但由于异步通信的设计,进入人员请求是有可能在电梯关了门之后才被电梯处理,此时应当拒绝该请求,由于将判断开门的条件写在了处理请求前而非处理请求中,导致在拒绝该请求的时候无法触发回复机制,人员丢失,并且当没有后续人员到来的时候,所有线程都无法再被唤醒。多线程编程中需要小心地考虑线程运行的状态,即使采用异步通信的模式,也必须仔细判断请求发送的条件。
五、分析自己发现别人程序Bug所采用的策略
采用自动评测、随机数据的方式进行功能测试,添加了部分极端数据。没有从源代码层面寻找bug。三次互测中共发现过2处bug。
六、心得体会
线程间协作时的安全问题是多线程编程的一大麻烦,线程间的耦合度越低越容易写出线程安全的程序,最好不要调用归其它线程管理的类的方法,尽量减少共享的可变资源,甚至像此次作业的设计一样不共享任何可变资源,可以充分保证低耦合避免线程安全问题。Command模式对于隔离调用者与被调用者,减少耦合有很好的作用。Strategy模式对于算法的抽象非常有效,可以提供便于替换的算法库,使得我有机会肆意对算法做了扩展与调整,在编程过程中充分体会到了采用良好的设计模式的优势,开发速度与效果都远优于上手直接瞎写。经过这一单元的学习,对于并发式的编程需要处理的问题有了一定的了解,也尝试了一些相关的处理手段,以及也被需求催促着读了一些相关书籍,收获颇丰。