Fork me on GitHub

OO_Unit2——电梯模拟

OO_Unit2——电梯模拟

1 架构设计策略

  1.1 Task1——单部多线程可捎带电梯

    • 前言:其实不只是程序员,事实上每一个等电梯的人,在看着楼层显示屏中的数字离自己越来越近时,都至少有那么一刻会不自觉的与电梯设计者进行一次思想的交互,会站在设计者的角度来想象、模拟电梯的运行策略算法,有时还会不禁暗自称道其设计思路的高明。这次作业让我有幸能根据自己平时的体验来亲自设计一款具有自己风格的电梯,作为一只每天穿梭于宿舍与图书馆之间(疫情期间除外)的贵系献祭者,自然对图书馆的电梯运行策略是再熟悉不过了,因此,本次设计的电梯就是基于自己经常搭乘的校图书馆电梯的设计思想,但是因为作业的性能度量基于电梯的角度,于是在捎带策略上牺牲了部分乘客的便捷性,从而实现整体运行时间最短。
    • 两个线程,一个用于输入乘客请求(Input类),一个用于电梯运行控制(Controller类)
    • 两个实时容器,一个存储电梯外正在等候的乘客(ReqQueue类 + Floor类),一个存储电梯类正在搭乘的乘客(Elevator类)
    • Input——ReqQueue类——Controller类 构成生产者消费者模式
    • ReqQueue采用ArrayList<Floor>来分楼层建立队列,并且在每一个Floor中建立两个LinkedBlockingQueue<PersonRequest>,分别存储向上运行请求队列和向下运行请求队列,队列排序采用先来后到机制,ReqQueue由Input线程的输入实时更新请求队列并存储至具体的Floor
    • Elevator采用TreeMap<Integer, LinkedBlockingQueue<PersonRequest>>存储其中的乘客,根据Integer(乘客的目的楼层),为每一个目的楼层建立一个乘客队列。同时Elevator类还记录的电梯当前的状态,包括当前楼层(currentFloor)、当前乘客数量(PassNum)。
    • Controller同时根据ReqQueue和Elevator中的乘客队列,来控制Elevator的运行,包括goUp、goDown、openAndClose、exchangePassenger。采用的捎带策略是:
      • 每到达一个楼层,就判断是否有乘客需要下,或者有同方向请求的乘客需要上,如果是,则exchangePassenger(包括相反方向的乘客,尽量避免下一次经过需要再开关一次)。
      • 每到达一个楼层,就综合当前内外乘客队列,利用方法findNearestFloor来挑选出距离当前楼层最近的一个需要开关门的楼层,将其设定为当前的toFloor(当前电梯目的楼层),toFloor也是每到达一层就更新一次。
    • 任务结束判断:在ReqQueue中维护一个isEnd标记变量,当Input输入为null时就setIsEnd为true并结束线程,当Controller检测到当前电梯内外乘客均为空时,就检查ReqQueue的isEnd,如果为true,就结束线程,从而结束整个任务。

  1.2 Task2——多部多线程可捎带电梯

    • 前言:本次的递进之处就在于,电梯的数量变为了多台,毫无疑问,从性能的角度来说这是一件很愉快的事情,但同时挑战与机遇并存。首先电梯的台数不是固定的而是randInt [1,5],其次电梯之间的交互具有很大的不确定性,一不留神就会出现同一个人进了多台电梯。在以上安全性问题上解决清楚了之后,要提高性能就得充分利用所有电梯,尽量让所有电梯都处于工作状态避免吃瓜电梯。
    • 负楼层问题其实它的本质就是电梯初始时不再像第一次作业那样只能上行,这次初始时还能向下走。解决这一问题的方法非常简单,我充分运用了OS中的思想,引入“虚拟楼层”与“物理楼层”的概念,通过vf2pf (int vf) 和 pf2vf (int pf)两个方法就能进行轻松转换,还能解决从-1到1跨两个数的问题,这样一来在写程序时只需要辨别什么时候用虚拟楼层什么时候用物理楼层即可。
    • 多电梯交互问题针对这一问题想过很多种思路,但都会遇到一个很棘手的问题就是多个电梯之间怎么在信息共享的同时又不发生冲突,最后得出的结论是:最安全的交互方式就是不交互。虽然这个思想有些矛盾,但却是很有根据的,考虑到上次写的程序无论从安全还是从性能上来说都可圈可点,并且我不愿意重复造轮子,因此这次我就直接照搬上次的程序,并在上次的Input和Controller之间插入一个总调度器(名曰:MainDispatcher),将上次的一对生产者---消费者升级为两对生产者---消费者,即上次的Input与本次MainDispatcher、本次MainDispatcher与上次Controller之间构成两层生产者-消费者模式。Input 将输入的请求存入队列容器MainTray,MainDispatcher 从队列容器 MainTray 中取出请求,并通过一个综合了一系列分配算法的getBestToLiftID方法,在电梯列表ctrlList容器中挑选出一个最优的顺路最近没满电梯,将请求分配给它的等待队列(每个电梯都有一个专属于自己对外部不可见的个楼层等待队列),接下来电梯的工作方式就和第一次作业没有任何区别啦。
    • 冥冥之中的巧合可见,通过一个总调度器的统筹分配,既能充分利用第一次作业的迭代,又能避免电梯之间因为交互而出现bug(一个请求只会被一台电梯所响应),可谓一石二鸟。这个方法是我在没有借助任何参考资料的前提下,通过自己的分析思考总结出的。不过在后来上的OS课程中,意外发现了一种多处理器的进程调度策略和我本次的实现思路竟然惊人地相似,如下图:                                                       简单翻译一下上图,就是将多个电梯看作了多个并行的从CPU处理器(虽然实则并发),它们各自拥有一个就绪队列,每个请求任务从开始到结束都在一个电梯(CPU)上完成,而MainDispatcher就是给这些从处理器分配请求(进程)的主处理器。看来这一思想早已得到了广泛运用,同时不得不感叹OO 与 OS冥冥之中的不解之缘orz。
    • 性能优化问题:通过以上的分析可见,本次作业的性能,基本取决于两个因素,一是分配策略的采取,即getBestToLiftID方法的实现方式;二是每个电梯自身的运行策略。后者因为在上次作业中已经体现出了较好的性能优势,因此可以直接移植于上次的作业,因此本次作业只需要关注于怎么实现请求的分配即可。之前也有想过用随机分配或者是轮流平均分配的策略,但因为实现方式上过于单调简单无脑,随机还具有性能不稳定性,因此我最终还是采取的多层筛选法,首先我会根据输入的电梯数目将楼层平均分为相应数目的部分,给每个电梯初始化一个目的楼层toFloor,使其在请求还没输入之前就还是运动,并均匀分散到各个楼层。每当请求来临时,这种分层筛选法的好处就是能够无限制的扩展下去,具有很好的可拓展性和可修改性,因为时间有限,我没有进一步做更加精细的筛选,目前的性能也还不错。经过之后的检验发现能够充分避免电梯的空闲,实现请求的基本平均分配。
      • 第一层筛选:遍历一遍每个电梯,挑选出合适的电梯,使得请求的 from 与 to 都包含在这个电梯的 currentFloor 和 toFloor 中,并且电梯与请求顺路,然后在满足这些要求的电梯中挑选一个离请求楼层最近的一个进行分配,如果不存在满足要求的就进入下一层筛选。
      • 第二层筛选:遍历一遍每个电梯,挑选出合适的电梯,使得请求的 from 包含在这个电梯的 currentFloor 和 toFloor 中,而请求的 to 在toFloor之外,但是是同方向的,然后在满足这些要求的电梯中挑选一个离请求楼层最近的一个进行分配,如果不存在满足要求的就进入下一层筛选。
      • 第三层筛选:遍历一遍每个电梯,挑选出合适的电梯,使得请求的 from 和 to 虽然都不包含在这个电梯的,但是这个请求的from在电梯 currentFloor 的前方且顺路,然后在满足这些要求的电梯中挑选一个离请求楼层最近的一个进行分配,如果不存在满足要求的就进入下一层筛选。
      • 第四层筛选(终结篇):在所有离该请求最近的电梯中,人数最少的那个,就进行分配。这次能够保证必有一个电梯被选中。

  1.3 Task3——多部可增限制楼层电梯

    • 前言:相对于上次作业的新增难点主要是电梯分类并限制楼层且可实时增加新电梯,而且对三类电梯的可达楼层设定怎么看怎么别扭,不过经过一定分析还是能找到一定规律(破绽)从而下手解决的。这次作业的总体结构和上次的相去无多,在Class的创建上和上次完全一致,同样还是上次那种两层生产者-消费者模式,只是在每个Class内部的实现方式做出了一些改动。
    • 电梯子调度器管理:在上次作业中是根据初始输入的电梯数在主调度器(MainDispatcher)中建立一个对应size大小的ArrayList<SubController>容器来存储其管理的子调度器,故将其命名为cltrList;而这次因为电梯被分为了A、B、C三类,因此就改为建立一个TreeMap<String,ArrayList<SubController>>容器来存储各个类别的电梯,故将其命名为ctrlMap,当收到新增电梯指令时,就在ctrlMap对应类别的ArrayList中添加一个电梯,这样一来,就为每一个类别的电梯维护了一个和上次类似的ArrayList。
    • 乘客请求分配算法:主调度器类(MainDispatcher)同上次作业一样用于从MainTray中取出由Input输入的请求并进行处理,不同点在于需要判断是新增电梯请求还是乘客请求,如果是新增电梯请求,就按照上文所说方式加入给ctrlMap;如果是乘客请求,则根据一定的请求分配算法,分配给合适的子电梯控制器进行运行,每个电梯的运行策略沿用Task1的方法。本次作业的核心部分和上次一样,都是对乘客请求的分配算法的设计。我的做法同样是多层筛选法
      • 第一层:按照A、B、C的顺序依次判断是否可以通过一部电梯直达,即调用 dispatchByNoTransfer(req)方法,如果能通过某一类电梯直达,就在该类电梯的ArrayList中挑选一个电梯内外等待请求最少的电梯并分配给它;如果不能直达,则进入第二层筛选。这么做的原因是:A、B、C的运行速度依次递减,本次性能不在仅仅看总时间,而需要考虑每个单独乘客的等待时间,而换乘会花费额外的开关门时间,对于单个乘客来说并不划算。另外,本次作业新增电梯上限为 3 部,平均下来,每一类电梯只会新增一部,也就是说每一类电梯的ArrayList的size平均也就是 2 ,因此对于在这 2 部中挑选哪一部的问题,并没有必要构思诸如 “ 最近通向非满 ” 此类算法来挑选,直接看那个比较空闲(人少)就给谁就好了。
      • 第二层:判断是否满足由 A 换乘到 B 就能到达(即该乘客请求的 from 是 A类 可达的,to 是 B类 可达的),如果可以,则将该请求拆分为两个请求,先由 from -> midFloor,再由 midFloor -> to ,midFloor为换乘楼层,将前一半请求重新从第一层开始进行分配,后一半(不妨称之为 “ 二次请求 ” )则存入一个 HashMap<Integer, PersonRequest>  secondReq 中,当前一个请求OUT电梯之时,再调用 dispatchSecondReq 方法,从secondReq中取出和这个请求ID相同的 “ 二次请求 ” 将其投入到第一层进行筛选分配,这样就充分避免了两个相同ID的请求在时间上发生冲突。
      • 第三层:相当于第二层的反过来,判断是否能由 B 换乘到 A 就能到达,如果是,则采取相同的请求拆分方式,拆分成两个请求按顺序先后分配。
      • 第四层:终于到了迫不得已只好用 C 类电梯来换乘的地步了(谁叫这厮是龟速呢。),幸运地发现唯独 3 楼是只有 C 类才可以到达的,因此,只有当请求的 from 或 to 为 3 时才用到 C 来换乘,而另一半换乘则是根据运行速度,先考虑A再考虑B。
    • 输出接口同步封装为了进一步保证线程的安全性,这次指导书新增了一个小提醒:“TimableOutput输出接口不保证线程安全,所以请做好处理”。既然是本次作业指导书中新增的提示,那么就让人不得不引起一丝警觉,经过很长时间的理解,其意思大概是,相比于普通的println方法,因为TimableOutput.println附加了时间戳,因此其内部的实现并不能像println那样视为原子操作,而输出接口API应该被视作一种公共资源,所以应该对其进行同步封装,所以相对于上一作业我新增了一个SafeOutput类,其具体实现如下:

2 SOLID分析(Task3)

  • Single Responsibility Principle 
    • 各个类与方法的职责分工较为明确,总调度器负责分配请求给子调度器,子调度器负责控制子电梯运行目的地,子电梯负责上下行和进出乘客,并保存电梯的乘客队列、楼层等信息。
  • Open Close Principle 
    • 三次作业层层递进,后一次作业都是在前一次的基础上移植拓展,并更新请求分配算法,而有关电梯运行调度的方法,基本没有做出修改,而是在其基础之上继续扩展。
  • Liskov Substitution Principle 
    • 因为并不存在父子类之间的继承,因此无需讨论。
  • Interface Segregation Principle 
    • 不需要实现接口,故无需讨论。
  • Dependency Inversion Principle

    • 在本次作业中,子调度器的调度,不需要依赖于其对应子电梯的类型,不同子电梯类型的区分仅在电梯类中可见。

3 度量分析

  3.1 Task 1

    • 经过合理的方法分解及类解耦,有效降低了复杂度,只有Controller.findNearestFloor(int currentFloor)存在硬伤,虽然它代码量仅仅只有20行,但是作为消费者,实现了队列为空时的线程等待,并且还有一个循环来获取当前最近的目的楼层。毕竟它是电梯运行及捎带策略的和核心部分,复杂度较高自然也很正常。


  3.2 Task 2

    • 整体复杂度还是可以接受的,其中:
      • SubController.findNearestFloor(int currentFloor)因为是继承于上一次作业,同时因为要考虑电梯满的特殊情况,因此和上次一样具有复杂较高的硬伤。
      • SubController.passengerIn(int)因为存在分支控制先下后上,何时只有同方向进入电梯,何时反方的也需要进入,会考虑到多种情况,因此复杂度较高。
      • Input.getInitDispatch(int)是一个根据输入的电梯数目,对各个电梯的目标楼层进行初始化以达到分散的效果的方法,是一个switch结构,因此复杂度较高。
      • 剩下的就是一些以getToList为前缀的请求分配策略算法,会对电梯进行遍历循环,并且有多重判断分支,因此复杂度较高。


  3.3 Task 3

    • 整体复杂度还是看得过去的,其中有个别方法复杂度较高:
      • SubController.findNearestFloor(int currentFloor)原因同上次一样
      • SubController.passengerIn(int)原因同上次一样
      • MainDispatcher.dispatchByC(PersonRequest)是用C类电梯换乘时的分配算法,需要考虑 from 为 3 楼还是 to 为 3 楼,以及采用 A 类电梯与其搭配还是 B 类与其搭配,因此会有较多分支语句,导致复杂度较高。
      • MainDispatcher.run() 这次的复杂度相较于上次有所上升,原因是它这次不仅处理乘客请求的分配,还需要处理电梯增加请求,同时线程结束条件也有所调整,不仅需要满足Input输入为null、MainTray队列为空,还必须满足 “二次分配” map为空,因此分支语句较多,判断语句逻辑较复杂。


4 Bug&性能

  4.1 Task 1

    • Bug:无。
    • 性能:(AVG = 96.4247)从性能得分来看,两级分化较为明显,当然这也难以避免,当追求某一类型的样例有较好的性能时,往往会存在一些极端情况与其不配合,在课下时不管用哪种调度算法,我总能构思出一种样例使该算法的效率低于其它算法。因此我选择了通过概率相关权衡尽量满足大部分一般样例的性能。


 

  4.2 Task 2

    • Bug:无。
    • 性能:(AVG = 98.0228)可见在上次单部电梯调度算法的基础上,添加总调度器请求分配算法,二者相结合,性能还是可观的。


 

  4.3 Task 3

    • Bug:有点小遗憾未能达成三次零bug,并且在互测刚开始就(有幸)成为房间中唯一一个被 hack 的选手Orz(当时心态就崩了),原因是在实现换乘分配算法时,因为拆分请求用到了些许递归的策略,而这种策略在一些极端情况下会出现StackOverflow,而正是因为出现这种极端情况的概率很小,以至于我课下构造的自动测评机都没有生成出导致栈溢出的样例,并且好在强测也稳住了没有翻车。
    • 性能:(AVG = 99.1016)不过这次性能的度量以及性能分的计算和以往不太一样,看到很多人的性能分普遍都比较高。

5 Hack策略

  • 三次作业都是采用自动化测试进行hack:
    • Task1:未测出bug,且房间所有成员均未被hack出bug
    • Task2:测出1个bug,房间总共hack出2个bug
    • Task3:未测出bug,且自己是房间中唯一被hack者
  • 总的来说,我的自动测试程序能够很有效的检查出样例对应的输出是否正确,但是又以下问题:
    • 自动生成样例程序的随机性较高,产生能够成功hack的样例几率极低,三次作业唯一hack成功的样例还是自己手动构造出的针对负楼层处理的样例
    • 自动化程度有限,每测试一次样例都需要人工进行一次操作,并且需要对代码进行修改以能够与自己的测评机对接,过程较为繁琐

6 心得体会

  • 对于架构的设计,特别是便于迭代的架构,有了较好的设计方式,设计风格相比于第一单元有了显著的提高,并且能够构思出较好性能的调度算法
  • 因为后一次作业总能直接在前一次作业的基础上做一些局部的修改来达到新的特性,因此花费的课下时间相较于前一次减少,因此coding过程体验较好
  • 对于线程以及锁的运用有了较好的理解与熟练运用,三次作业全程未出现线程相关的bug
  • 对容器的使用也有了扩展,开始使用诸如阻塞队列等实现较复杂的队列容器
  • 对于用python搭建测评机有了一套较为熟练的方法,但是还并不习惯于在命令行进行bat相关程序的测评机搭建,因此自动化程度还很有限,下一单元的作业需要着重提升自动测评机的自动化程度,希望能实现全自动样例生成、结果测试、bug提取的一条龙连贯全面全自动测评机。

 

posted @ 2020-03-29 00:16  LarryHawkingYoung  阅读(353)  评论(0)    收藏  举报
Live2D
/* 点击爆炸效果*/