面向对象第二单元博客作业

第二单元博客作业


架构设计

三次作业的架构设计基本上保持不变,基本上只是完善了前一次作业留出的未完成的函数。

  • 因此合并三次作业的架构设计,有关迭代的部分使用这样的形式进行表述。

电梯决策设计

首先是电梯决策方式设计,为了可拓展性考虑,将电梯的决策行为抽象为Strategy接口,接口中留出getDirectionpickUser方法。这样无论是采用集中调度,或自由竞争,均可以使用不同的接口类来实现所需效果。

在实现中,不同类型的路线策略可以在策略类和路线类中实现,因此对于任意路线,电梯类的内容不需要改变。

电梯路径设计

同时,由于每层每座之间没有实质性的区别,同时考虑到线路的可拓展性,将每条电梯线路抽象为Route类,采用链表记录途经节点,每个节点记录自身的可达性和楼层信息,可以适应环形路径、部分楼层不停靠等复杂情况。为电梯留出nextFloor方法用于获取下一个楼层的楼层信息。

用户容器设计

我们的作业是单生产者多消费者的情景,因此保证生产者生产出来的数据的线程安全十分重要。出于对楼层的模拟,将每个楼层作为一个容器,并维护该容器的线程安全。

  • 在第六次作业中抽象了电梯对容器的行为,使用函数式接口Predicate<User>,抽象出三种方法供消费者线程使用,分别是has,peek,get方法,分别是获取楼层是否存在满足条件的用户、获取楼层满足条件的用户但不移出容器,获取楼层满足条件的用户并移出容器。这样做使得对容器的操作行为被分离出容器,降低了容器和线程间的耦合度,降低了容器类的复杂度。

对于线程安全的考虑,对于每个楼层的容器单独加锁,本次作业中采用普通的synchronize同步锁。单独加锁的设计类似其它线程安全容器中的分段锁,每个楼层的容器同时只能被一个线程访问。同时对于同路线的不同线程,线程通过路线对象的同步方法查询楼层情况,路线对象通过调度中心对象查询楼层情况。这样实现可以使不同电梯间等待锁的时间较短,同时保证了同路线电梯决策的正确性。

调度中心设计

调度中心的设计灵感是从本单元第一次实验获取的,调度中心本身不作为线程,考虑到换乘与新需求输入可以抽象为同种行为,调度中心向外只暴露addUser方法,该方法输入了当前乘客的起点和终点信息,计算乘客换乘路径后(本次作业不涉及)更新乘客当前目的地将乘客放入到对应楼层容器中。电梯线程在完成一段路径后更新乘客当前位置将乘客重新加入到换乘中心,重新调度。

调度器自身并不作为线程,而是依托与输入线程和电梯线程而运行。

  • 在第六次作业中,对于电梯线程的输入和初始化,统一由调度中心完成。输入线程获取到增加电梯的消息时调用addElevator方法,由调度中心新增线程并运行。
  • 在第七次作业中,加入了当输入新的电梯时,对所有用户进行重新规划路线。

调度策略设计

本次实验中,采用了部分自由竞争的Look策略,在电梯线程到达新的楼层后,获取方向并被分配本层内的某一方向用户,在开关门间隔内到达的用户也会被分配给该电梯。每层用户最多由两个电梯线程获取(在运力足够时)。当本层没有用户时,所有电梯均向最近的需求方向前进。当本路线没有用户时,所有电梯等待。

  • 在第六次作业中,对于环形路线的情况,采用Look策略会因为前方始终有需求导致无限循环的情况。为了防止这种情况,在环状电梯的Look策略中加入了如果本层有反向用户且电梯内目前没有用户则优先响应本层反向用户的请求,如此便可避免循环。

电梯在到达新楼层或被wake后重新开始循环,在所有用户需求未完成时,从策略接口获取移动方向,如果没有需求则wait,在本层没有可接新需求后移动至下一节点。被wake的情况有两种,一种是调度中心发来了本路线的新用户需求,另一种是调度中心发来了输入线程终止的信号,此时重新开始循环判断已结束,结束线程。

架构分析

拓展性分析

目前的架构有一定的拓展性,使得三次作业中没有大的重构。仍可以支持拓展的部分有:

  • 电梯路线任意设定:如A-1 -> B-3 -> D-8 -> A-9 -> ...
  • 电梯策略插拔式替换(接口)
  • 调度策略插拔式替换(接口)
  • 人员接送优先级设定
  • 人员路线保存与查找
  • ...

协作关系分析

类协作图如下:

测试

本地测试

本地使用带特征的随机样例进行并发测试,每个用户的测试进程数可调。支持CPU时间、总时间及总等待时间计算及模拟得分,用户错误信息/样例记录,测试集用户表现统计。

互测中采用8*20并发进行测试。

测试数据特征大致有:

  • 请求/目的地聚集度
  • 需求/电梯数密度
  • 楼座/单电梯压力
  • ...

程序Bug

此处只叙述两个Debug难度较大,不易复现的Bug。

在第五次作业中,存在1个没有发现的Bug,在公测和互测中均没有被发现。

在第六次作业中,上述Bug仍然存在,在公测和互测中均没有被发现。

在第七次作业中,在课下测试中发现了2个Bug。

  • 第一个Bug是在优化时出现的Bug,出现的原因是,当新电梯到来时,会为所有乘客规划路线。当某个一层的电梯正在决策时,刚刚发现了路线内有需求,需要初始化方向时(前往需求较多的方向,否则随机)。在先前自己的逻辑中一楼的电梯只可能初始化向上,不可能出现随机方向的情况,但是在新加入电梯后,原先乘坐该电梯的乘客被分配到其它路线,导致初始化随机方向向下,非循环电梯没有对应方向的可停站点从而报错。
  • 第二个Bug是第五次作业遗留的Bug,出现的原因是,当新用户请求到来时间恰好处于对应电梯决策完毕,还未开始wait()时,此时新用户请求在wait()前对电梯线程进行唤醒,但电梯随后仍然会一直等待,导致超时。这个问题出现的可能性很小,导致在之前两次作业中没有被发现。

两个Bug均为线程协同问题的Bug,多线程相关的问题还需要仔细思考,不断完善。

互测

第五次作业中发现若干输出线程不安全、策略死循环Bug。

第六次作业中发现若干遍历时修改、无法结束的Bug。

第七次作业中发现开关门时间不足(浮点数误差),无法结束,策略性能过低的Bug,但由于策略Bug复现率太高(80%+),其它Bug复现率相对较低(20%),导致除策略Bug外均未能成功Hack,并且由于重测机制问题,对应的测试点没有能够得到重测,使得多个问题程序顺利通过互测,未能在电梯单元结束时发现自己的Bug。这些Bug永远的封存了。希望多线程这里的互测机制可以进行修改。

  • 其中,这些没能复现的Bug在本地100并发小数据中产生的概率在20%左右,浮点数误差在大压力10并发测试下产生的概率在70%左右,但很遗憾,互测的数据限制较为严格,即使是20%复现的小数据,提交10次也未能Hack成功,互测由于数据被其它策略问题程序影响导致未能重测。

心得体会

多线程场景下的问题往往是自己没有考虑到的,除了多加测试外,只能通过更精密的设计来尽可能避免程序有问题。我们还需要多加积累,不断锤炼自己的代码水平。

此外希望公测的强度可以进行提升,互测/重测的机制可以进行修改,以免让Bug始终躲在我们电脑的一角。

posted @ 2022-04-25 23:39  raspstudio  阅读(41)  评论(1编辑  收藏  举报