2022面向对象设计与构造课程第二单元作业总结

综述

本单元的作业以电梯的载客与运行为背景,让同学们初步熟悉并掌握了多线程的面向对象编程要点。

在本单元中,我认为架构和线程安全是两个齐头并进的重要组成部分。

任意一方面出现的小问题,都有可能导致测试结果出现较大的差错,两个方面都不能懈怠。

因此,我觉得第二单元总体难度还是较第一单元更难一些的。

历次作业分析

总体上,在本单元作业中,我构造了三种线程类,分别为输入处理线程、电梯线程、输出处理线程。

依次形成了两组生产者-消费者模型,用中央控制器类中的两类队列进行交互。

先是输入处理线程将人的达成需求投入中央控制器,经调度处理后分配到所选择的电梯对应的载人队列中。

当电梯检测到有新的请求时,会从中央控制器获取请求,并进行一系列行为去满足搭载请求。

在电梯运行期间,会将需要输出的信息在对应时刻投放到中央控制器的输出队列中,并等待返回时间戳。

输出处理则在有输出信息要处理时每次从输出队列取一个输出,进行输出后返回时间戳,从而保证输出不乱序。

其中,中央控制器的所有被外界调用的方法都被synchronized修饰,使得每次外界线程与中央控制器的交互都是原子性的,从而保证了线程安全。

第五次作业

UML类图及协作图如下:

第五次作业中,所考虑的场景还是较为基础的,每座只有一部电梯,进行同座的搭载任务。

在本次作业中,中央控制器只负责简单地将输入线程投入的请求按照楼座分类进入队列中。

而电梯的运行采用了LOOK算法,一直只接同向搭乘的乘客,直到无需继续同向行驶时换向,易于编写,同时具有一定效率性。

主要的难点在于电梯切换运行方向时的判断,需要进行优化,在尽头时要能够接入反向乘客,并防止同层两次开门。

第六次作业

UML类图及协作图如下:

 在第六次作业主要在于新增了能够循环在楼座间运行的横向电梯,并且同一座/层可以出现多个电梯。

 

对于横向电梯,由于是循环运行,我在LOOK算法的基础上,加入了一个描述当前要运行到的楼座的target变量,并进行维护,暂且命名为TARGET算法。

当同向运行到某需要有乘客进出的楼层更近时,会优先同向运行并更新target,直到不再更新并抵达target楼座时,换向运行。

这种电梯运行算法有效利用了楼座的循环性质,可以大幅减少不必要的时间开销,但同时也带来了重新设计算法的问题,容易出现算法上的BUG。

 

而对于多个电梯,我增加了一个ElevatorList类,里面包括某层或某座电梯组成的一个Arraylist和用于循环计数的变量cnt。

在搭乘请求的投放上,我没有采用自由竞争的写法,原因是我在电梯运行算法的写法上要求每个电梯必须能载到投递给它的所有请求,空开关门可能会出现意料之外的BUG。

秉持着迭代大于重构的原则,我将调度器的请求分配进行了迭代,采用由调度器将每个请求必定选择某一电梯投放的算法。

可以从流程图中看到,会基于一定算法选择出一个被认为相对最佳的电梯。

在第六次作业中,我优先投放给在请求所要求搭载的楼层/座电梯中能够同向接上请求的电梯中目前要完成的请求最少的一个,如果不存在同向,则在对应电梯列表中轮流投放。

这样由中央控制调度的方式可能牺牲了局部上自由竞争的效率,但也可能避免一台电梯过度抢夺请求,使得整体调度速度提升,具体视数据情况而定。

 

新增电梯的请求则直接由中央控制器处理,将新增电梯从主类移动到了中央控制器内进行处理,这部份相对比较简单,不做赘述。

第七次作业

UML类图及协作图如下:

 

第七次作业中主要的改动需求有以下几点:电梯的容量、速度可变,横向电梯开门楼座限制,请求会需要换乘。

首先,电梯的容量限制并无大碍,只需为电梯类增加一个容量变量及在满载判断处修改即可。

而其余的三个变化,即速度可变、横向开门楼座的限制、请求的换乘,我都通过中央控制器请求投放和分配策略的算法重构进行了解决。

 

接下来便对第七次请求分配策略进行说明:

首先,当遇到一个请求时,先判断是否在同一楼座,如果是,则同第六次作业处理。

如果不是,那么就将这个请求拆分为三段请求。

首先根据每层横向楼座电梯的开门限制,寻找到可以换乘的楼层。

然后与标准策略类似,将目标中转楼层定为起始楼层->中转楼层->目标楼层这一过程中纵向移动距离和最短的楼层。

最后,将请求依次拆分成三段,即起始楼层->中转楼层,起始楼座->目标楼座,中转楼层->目标楼层,这样,每一段请求被化成了第六次作业的形式,可以直接投放。

至于为什么不进行多次楼座中转换乘,是考虑到这样同样有可能造成多次开关门进出的时间消耗反而比远距离运载时间成本高的情况,不一定更优,故没有进行采用。

 

这样,将原本直接调用addRequest投放请求改为先调用analyzeRequest拆分,就解决了请求的换乘问题。

接下来便是拆分后投放请求时的分配算法的重构。

由于速度上的差异,按照第六次的分配模式显然是不优的,需要进行改造。

由于在拆分请求时就保证了投放的目标楼层/座必须要能够满足换乘的需求,第一步便是找出能够满足换乘的电梯(同楼座即全部都可以换乘)。

在这些满足条件的电梯中,与第六次作业类似,如果有同向可顺便搭载的,那么就优先在这些同向电梯中选择,否则就仍用全体可换乘的电梯。

接下来,定义电梯负载为一部电梯目前等待被搭载的人数与正在被搭载的人数之和。

以电梯速度(移动一层所需时间)与负载的乘积作为指标,在被选出的电梯中选择该指标最小的进行请求投放。

通过这样的算法重构,就解决了电梯速度不统一的问题,同时经过这两个阶段的请求分配,也使得横向电梯必定运输自己能够运输的请求,满足了其开门限制。

个人认为,这种调度算法充分考虑到了速度与单电梯负载过多之间的平衡,能够自动避免在一个电梯上投放过多请求,也能够相对地给速度快的电梯更多请求。

可能在指标的选择上有不够优的地方,但总体上我认为效率还是相对不错的。

测试及BUG总结

本单元中,主要采用了手工构造样例和随机样例生成结合的模式,但仍然出现了不少未发现的BUG。

 在输入输出顺序上,由于我保证了所有对于共享资源的访问都是同步的,所以并没有出现顺序错乱的问题。

 

主要的BUG还是出现在算法上:

其中一种BUG产生在电梯启动的时刻,由于在算法中我默认了电梯从等待变为开门必然是在当前层有人要进入,因此只要当前层有人就会开门。

但是判断启动方向的时候采用的仅仅是等待队首的请求方向,如果这个请求并不在当前层又与当前层请求方向相反,就会接不上当前层的请求,在接下来的启动过程中发生取空指针的错误。

在无论是在第五次作业的LOOK算法还是第七次修改后的TARGET算法都会有类似问题的出现。

解决只需要增加判断条件即可。

主要未能发现的原因在于电脑线程与评测机线程调度差异较大,在本地很难复现两个请求同时进入队列的情况,导致难以查出。

 

另一种BUG则是第六次作业中TARGET算法出现的问题。

由于优化策略设计得较为简单,导致可能会出现电梯循环更新运行目标楼座的情况。

在电梯空载运行时,算法会判断同向接到下一个人是否比反向运行更近,能的话就继续同向运行,直到接到人再转向。

此时,就会导致如果五个楼层全部都有反向请求,那么电梯会一直认为接到下一个人比现在的状态优,那么会循环运行下去,而永远无法反向接人。

这是由于设计算法时考虑不够周全产生的,在第七次作业中采用重构算法的方式来解决,为了弥补而花费了不少时间。

 

比较可惜的是在第七次作业中,只对新增情况进行了测试,对老样例测试不足。

这导致对于同楼座的请求在乘客由于在一开始就没有进行分段,导致乘客出电梯时找不到下一段的请求安排,会发生空指针错误。

第七次强测中由于这个可以通过一个简单判断解决的BUG造成了大量样例无法通过,丢失了将近一半的分数,非常遗憾。

总结

第二单元总体上不像第一单元一样将难度集中在了第一次到第二次的迭代中,而是在每次构建时都有一定程度的修改。

而且对于个人来说,线程的控制和交互确实是相对新接触到的概念,从接触到认识、掌握还是需要消耗相当多的时间。

因此我认为第二单元还是比第一单元难度更高一些,对于同学们多线程任务的训练也能够达到应有的效果。

posted @ 2022-04-30 19:12  丈二武士  阅读(30)  评论(1编辑  收藏  举报