面向对象设计与构造第二单元博客作业

第一次作业总结

UML类图

 

UML协作图 

 

架构与可扩展性分析

架构分析

本次作业采用了输入-调度-处理的结构。采用生产者-消费者模式,不同环节之间相互独立,仅通过共享托盘进行数据的传输。

复杂度分析 

类的规模,类与方法的复杂度如上。

同步块的设置和锁的选择

 全部统一使用了synchronize(obj)的方式进行上锁。为将线程等待其他线程释放锁的时间降到最低,在锁内部只进行数据的存取操作。取操作中,先把所有数据都存到线程私有的队列中并清除取走的数据,释放锁后再对私有队列进行处理。存操作中,先将数据全部处理好,获取锁后只进行add操作。根据架构设计,在输入线程RequestBuilder和调度器Controller之间 ,调度器Controller和每个电梯Elevator之间各有一个托盘WaitedRequest,并提供了addRequest()getRequest()clear()三个方法用于数据存取,inputEnd()isEnd()等方法用于判断线程终止。

对于输出线程的保护,最好的方法应该是新创建一个Output类,然后设置一个静态方法并加锁。但因为是第一次写多线程,怕对象和方法锁搞混弄出问题,就在电梯类里设置了个静态对象继续用的对象锁。后面因为输出基本没变化也就没再改了。

线程设计与线程交互

调度与运行逻辑设计

电梯运行基本遵照look策略,在电梯内有人或者当前运行方向的前方有请求时不会转向,否则转向。捎带时,只会检测当前楼层中与运行方向同向的请求。调度器根据请求的楼座,分派给不同电梯。

调度器交互

本次作业只需要一个调度器。该调度器先与输入线程进行交互,取走所有输入进来的请求。处理好请求与对应的楼座号的关系后与各个电梯交互,将请求放入托盘中。

测试与bug

 测试数据构造

与另一位同学合作,在往届代码的基础上构建了一个自动评测机。其中,本人负责数据生成器的开发,数据正确性检验部分由另一位同学完成,模拟投喂以及测试框架基本沿用。本次的需求较为简单,因此只设置了以下两种模式:

1. 完全随机楼座与起点、终点,主要检查功能的完备性。

2. 固定楼座,主要检查电梯运行的正确性。

自身bug分析

自测过程中未发现bug。强测和互测中同样均未被发现bug。

互测他人bug分析

本次作业仅发现了一个线程不终止的bug。

第二次作业总结

UML类图

 

UML协作图 

 

架构与可扩展性分析

架构分析

本次作业的架构沿袭第一次作业的架构进行了扩充。在第一次作业的基础上结构新增了一层,原来的调度器功能基本不变,作为主调度器进行楼层、楼座的划分。新增的两个调度器BuildingController和Floor Controller,对楼层内的任务再进行具体电梯的划分。针对不同阶段传输的请求不同,将托盘也扩展成了两种。原来的作为托盘基类,针对层/座调度器之前的,新增对于ElevatorRequest的传输,对于调度器和电梯之间的,新增对于电梯运行状态的传输。此外,考虑到后续可能的换乘问题,提前做准备创建了MyRequest类,将PersonRequest类重新进行封装。

复杂度分析 

类的规模,类与方法的复杂度如上。可以发现,主要的复杂度集中在了楼层与楼座的调度器上。

同步块的设置和锁的选择

因为第一次作业中锁的设置未发现bug,性能也较好,因此没有做出本质上的改变,对于新增的共享对象,继续沿用第一次作业中的锁。

线程设计与线程交互

调度与运行逻辑设计

纵向电梯的基本运行策略与上次一致。

对于新增的横向电梯,对使用的look策略进行了改进。由于是环形并且座数只有5个的特殊性,有以下特性:当某任务A是反向并且移动距离为2时,如果电梯内部已有任务,可以将A接收进电梯,然后继续前进。当到达下一座时,该任务的方向就会变成同向,从传统look认为的不可捎带任务变成了可捎带任务。经过自测检验,此优化基本可以稳定为正优化。

对于纵向调度器,以 超载-距离-已分配任务数 的优先级顺序进行判断,决定分配给哪个电梯,并将该任务传输至托盘中。

对于横向调度器,考虑到五座之间距离较短,且已做了上述的特殊优化,因此距离信息未再进行考虑,直接进行平均分配。

调度器交互

新增调度器沿用了第一次作业的交互逻辑,使用和第一次相同的方法对新增的托盘进行上锁。

测试与bug

 测试数据构造

基于第一次的生成器,结合第二次的数据需求继续进行迭代开发与改进。经过和其他人讨论,最终决定了以下几个模式:

1. 完全随机的楼层与楼座,主要测试功能的完备性。

2. 固定楼层、楼座,主要测试对于同一楼层/楼座内多个电梯的任务分配与调度

3. 固定时间,主要测试同一时间内出现大批量数据时的情况。

自身bug分析

自测过程中,经过评测机大批数据测试,发现新增横向电梯在关门前第二次判断进入信息时,如果已经满载,那么从托盘中取出的请求不会进入任何队列中,而是直接被忽略。经过自测,强测和互测中均未被发现bug。

互测他人bug分析

本次作业仅发现了一个输出线程不安全导致的bug。原因在于有部分语句忘记通过锁来进行输出了,猜测是第一次作业中的遗漏之处。

第三次作业总结

UML类图

 

UML协作图 

架构与可扩展性分析

架构分析

本次作业的架构基本沿袭了第二次作业的架构。因为在第二次作业中就已经提前做准备创建了MyRequest类,将PersonRequest类重新进行封装,所以本次作业中没有添加任何新的类,仅在各个类中为了满足新的功能添加、修改了一些变量与方法,并对实际运行策略进行了对应的修改。从第二次到第三次的扩展较为顺利。

复杂度分析

 

类的规模,类与方法的复杂度如上。其中因为调度器需要针对任务内容与电梯的状态进行分派,因此涉及策略的部分复杂度较高,其余部分复杂度都很正常。

同步块的设置和锁的选择

因为主调度器终止条件不再随着输入线程终止而终止,因此起初想要通过主调度器在合适时机遍历其他座/层调度器以及电梯的托盘来决定是否终止,但考虑到逻辑较为复杂,所以最后改用计数器实现。因此在线程交互上没有做任何改变,故该部分与上次均完全相同。

调度器设计与线程交互

调度与运行逻辑设计

基本运行策略以及调度策略与上次一致。对于主调度器切分换乘请求,考虑到在大批量随机请求下局部贪心计算最短路的结果意义不大,因此不考虑多次换乘横向电梯的情况,仅将其切分为三段式的纵-横-纵请求。对于层/座调度器调度任务到不同电梯的策略,新增对于电梯速度的考虑,将其作为系数乘到先前对于距离的判断上,以在分配上更倾向于把请求交给速度更快的电梯。

调度器交互

第二次作业的调度器以及线程之间的交互全部保留。同时,在第二次的基础上,为满足经过切分后的换乘请求不断更新阶段并被调度,新增了电梯与主调度器的交互。该交互沿用了输入线程与主调度器之间的托盘,将电梯视为和输入线程类似的生产者,所有从电梯中出来的请求全部输入进该托盘中,再由主调度器继续进行调度。

测试与bug

 测试数据构造

基于第二次的生成器,结合第三次的数据需求继续进行迭代开发与改进。经过和其他人讨论,最终决定了以下两个模式:

1. 完全随机楼层与楼座。主要用于判断是否所有座/层都有被考虑,以及简单的功能评测。

2. 随机数固定3个楼座以及2-3个楼层,并限制横向电梯数的最大值(5个)。主要用于评测新增功能的实现。经互测检验该模式强度远强于强测强度

 自身bug分析

自测过程中,发现请求在经过切分后,不同阶段更新的内部逻辑存在问题。纵-横-纵中,没有第一次纵向移动的请求会执行两次横向部分的请求。经过自测后,强测与互测均未发现bug。

互测他人bug分析

互测过程中,主要发现了4个bug,其中有3个在线上被复现出来,另外1个经过5次提交均未被评测机复现。

4个bug中,被复现的3个bug分别为:容器在添加元素前就因为使用get(0)方法导致越界访问(10次提交命中10次,也不知道怎么过的强测)、新需求导致线程终止条件改变从而出现无限等待(涉及线程访问共享对象的顺序,三次提交后命中)、电梯移动错误。

未被复现的1个bug为:当电梯完成任务的某一阶段后,会先将任务重新放到共享队列中,释放锁后再输出out信息,此时有可能出现其他电梯先检测到此任务并输出in信息,导致in和out的输出顺序调换。此bug复现难度较大,同一组数据本地复现需要5-8次不等。提交5次均未复现后遂放弃。

总结与心得

多线程程序的编写

多线程程序相比于单线程主要体现在多个线程交互导致的线程安全问题上。对此,主要有两种方法可以解决,一种是利用synchronize(obj)或者synchronize method的方式进行上锁。这种方法写起来比较简单,只需要将代码块用关键字包裹起来,注意AB-BA这类的死锁以及wait产生的可能无限等待的情况即可。但相对的,锁的类型比较单一,任一线程获取到锁后,其他线程都禁止进入。另一种方式是利用Lock类设计锁。这种方法写起来相对复杂些,但锁的形式更加多样。比如所有读的操作不会互相阻止进入,只会禁止写的线程进入。这样就比较适合一写多读的这种情况,可以一定程度提升交互的效率。本单元作业中,我基本都是采用的单一生产者-单一消费者的结构,只有电梯-输出与第三次作业的输入/电梯-主调度器属于多生产者-单一消费者。在这两种结构下,synchronize的方式在性能上已经足够,因此没有使用Lock的方式。

此外,多线程和单线程的另一个区别在于运行逻辑上。就和计组中单周期cpu和流水线cpu的区别一样,多线程中的每一个线程都可以理想的认为在同时运行(多核情况下),因此可以参照流水线来进行层次化设计。每一个任务的完成都可以切分为不同阶段,比如本单元作业中分层次调度以及分阶段换乘。

多线程程序的测试

相比于单线程,多线程测试更加困难。在debug过程中,通过断点来定位bug比较困难,而用println()的方式输出不同地方需要的信息来代替断点更加合适。

在测试中,因为线程运行交互的顺序以及时间不定,因此像第一单元那样构造一些小巧的测试点就没有太大意义了。即使通过阅读代码发现了问题,也不好手动生成测试点。因此,根据要求批量生成测试点来进行测试的方法更加有效。

posted @ 2022-04-29 11:19  alonelysnake  阅读(14)  评论(1编辑  收藏  举报