多线程协同与同步控制总结
- 第一次作业·多线程电梯
初次多线程设计,我根据老师的建议,分别建立了请求模拟器、调度器与电梯运行三种线程。请求模拟器处理输入后将有效请求放入托盘中;调度器的大循环首先扫描托盘,将新的请求加入请求队列,再扫描三部电梯的信息,得到快照,最后循环遍历队列中的所有请求,进行调度,改变队列请求状态与电梯信息类中的请求队列;电梯运行仅根据自身对应的电梯信息实现运动,同时改变电梯信息。为了多线程的安全,将队列仅作为调度器的私有属性,避免多重操作;将电梯信息类的所有方法用synchronized上锁。
- 第二次作业·IFTTT
本次作业涉及文件操作,考虑到File的不安全性,设立线程安全的SafeFile类,所有方法用synchronized上锁,在创建对象的同时获取信息快照。监视线程遵循“每个监控对象的每个触发器为一个线程”的原则,每个线程循环扫描文件树得到快照,通过新旧快照的对比判断是否触发,若触发则完成响应任务(包括记录与还原),最后更新快照以便下一次判断。
- 第三次作业·出租车系统
本次作业的线程主要分为三类:出租车、调度器、输入。首先是100个出租车线程,由于包含了共享信息,考虑到线程安全,出租车的信息快照方法上锁避免多重访问。其次是调度器线程,每次循环首先扫描全体出租车得到快照,接着遍历请求队列,依次判断是否已完成、是否在抢单窗口时间内(是则指挥抢单)、抢单窗口是否结束(是则根据该请求已接收的出租车信息决定派单)等,最终改变出租车及队列的状态。输入线程即为主线程,循环接受输入,检查合法性与同质性后加入队列,因测试接口的设计,输入线程也需要访问出租车快照,以便随时输出指定信息。队列及请求类的所有方法都上锁以确保线程安全。
度量分析
- 第一次作业·多线程电梯
类属性个数:46(含所有父类)
方法个数:80(含所有父类及接口)
类总代码规模:801
OO度量:
类图:
时序图:
自我点评:
从度量上看,出现了与上次一样的问题——圈复杂度与嵌套块深度,集中在调度器与电梯运行类中。因为两个类的线程run函数中操作较为复杂,与多个其他类有调用的关系。
从类图上看,线程共享资源即队列、电梯信息类中的方法较多,尤其是后者。细看会发现都是电梯性质的“set”、“get”型方法,由于电梯本身是整个程序运作的主干,信息量较多,造成了冗长的方法列举。
从设计原则角度看,每个类的职责并不单一,由于经验不足,框架不断修改,导致每个类的逻辑难以封闭,不够稳定(SRP);频繁使用常量,不符合显示表达原则。
- 第二次作业·IFTTT
类属性个数:27
方法个数:55
类总代码规模:821
OO度量:
类图:
时序图:
自我点评:
从度量上看,第一个问题还是圈复杂度与嵌套块深度不合格,集中在监视线程与输入处理类。主要原因是监视线程没有根据触发器条件、任务类型归纳为相应方法,而是直接一股脑全放到了run里,冗余重复的内容较多,且反复调用文件类、记录类,导致嵌套较深。第二个问题是监视线程初始化时的变量太多,换句话说责任太多,可以窥见整体的代码架构不平衡。
从类图上看,以监视线程为核心,文件安全类作为信息载体,共享资源为文件树,通过文件安全类获取快照时可保证信息的相对稳定。
从设计原则角度看,每个类的职责不单一,监视器线程需要实现扫描、判断触发、调动甚至自主完成任务(recover)等,不符合SRP原则;监控器里不同触发器的共同方法和属性罗列冗余(仅仅是为了理清思路),没有实现代码重用,违反了重用性原则。
- 第三次作业·出租车系统
类属性个数:52
方法个数:44
类总代码规模:1036
OO度量:
类图:
时序图:
自我点评:
从度量上看,除去无法改变的gui类,输入处理、出租车以及地图类圈复杂度较大,调度器与出租车的嵌套块深度较大,请求类的参数较多。输入处理由于合并了测试接口的功能,导致判断流程复杂度升高;地图类由于需要遍历map.txt文件的6400个点并依次处理信息,数组规模较大;出租车类对不同状态的相似处理没有合并归纳,且使用了bfs方法获取最短路径,导致嵌套层数变多;调度器的代码行数反而不多,原因是调用了请求内部的方法,通过它来访问或改变出租车,当初是考虑到该方法仅限于调度器调用,不会出现多线程同时访问,且请求内部信息更好获取,但导致了比较严重的结构缺陷。
从类图上看,未显示的map类负责读取地图文件的信息,建立出租车运动的路线基础;调度器与输入共享出租车与请求队列。
从设计原则角度看,和前几次作业一样,没有遵循SRP、DIP以及重用性原则。
BUG分析
- 第一次作业·多线程电梯
公测:全部通过
互测:安全通过
调试:在debug的过程中,主要出现的问题是线程分别运行,实时的变化导致信息获取前后不一致,解决方法是在调度器循环最开始加入了电梯信息的快照获取,保证信息处理时段相对静止。
测试程序:本次作业分配到的作业的bug较为严重,除了公测的输入处理测试可以通过,其余性能测试均会出现如Exception、时间错误、请求顺序错误、捎带判断错误等问题,测试较为困难。
- 第二次作业·IFTTT
公测:全部通过
互测:当目录广度与深度分别为20000时,程序运行出现问题。该压力测试主要暴露了程序在SafeFile类获取文件树快照时采用的递归算法在数据量较大时暴露出来的性能缺陷。
调试:本次作业情况较为特殊,在ddl前一天晚上才完完全全实现了全部功能,来不及仔细调试就急急忙忙开始写文档了。第二天对于作业的调试也机器有限,用测试线程简单测试并未出现明显错误。
测试程序:本次测试任务的问题很明显,首先是只要与目录有关都会出错,包括触发器无反应,任务未完成等。针对recover任务,有时会识别错误应该还原的对象,导致recover结果偏差,可能是理解上的失误。
- 第三次作业·出租车系统
公测:全部通过
互测:报错原因是gui显示的某些出租车运动路线不符合随机同分布原则,问题应该出在random生成随机数方面。
调试:由于出租车状态变化较快,测试范围有限,所以没有发现太严重的方向性错误。
测试程序:本次测试任务的分配算法基本正确,但是没有忽略出发点与目的地相同的情况,导致出租车在行驶时出现了空指针Exception。
心得体会
本单元的三次多线程作业,概括来讲就是:难度很大,细节很多,调试很玄。
高难度体现在编程上。比如对于“锁”、“线程安全”、“sleep”、“wait-notify”这些概念或是语法的理解与运用;线程共享资源保护,尽量缩小共享范围;引入“快照”概念,使动态信息相对静止;考虑问题多维化,要涵盖某一时刻所有线程的状态和行为;遵循SOLID原则使程序更优化等。
多细节体现在任务理解上。三次作业的任务都很繁重,多线程电梯需要结合请求队列和电梯状态进行选择分配,IFTTT需要完成四种触发器和三项任务,出租车系统需要调度100个出租车线程实现抢单派单。记得第一次多线程电梯的流程结构画了几张草稿纸,每次都是发现某一环节的不可行性而推倒重来,IFTTT则是在周一晚上才最终改成了正确的架构。除了整体框架,还有不可忽视却很模糊的小细节比如判断条件、时间、边界、操作标准……为了不漏掉只能终日游荡在答疑群和issue区,经历着“啊我又写错了”、“难道不是这样吗”、“幸好幸好”的过山车式心情起伏。
调试应该不用多说了。System.out真好用。
虽然曾在崩溃的边缘徘徊,最终还是顺利存活。感恩。