BUAA-OO-unit-2-总结

BUAA-OO-unit-2-总结

第二单元主题为实现多线程的实时电梯模拟,研究探索多线程协作、线程安全、设计模式等问题。

电梯单元的主要难点在于,在保证线程安全的前提下,设计出易于扩展的架构,同时保证性能。对此,我采用了单级托盘集中式调度的架构。三次作业迭代增量开发的过程中,我在电梯之间的宏观调度上主要采用自由竞争的策略,在单个电梯微观调度上则探索了不同的算法。实验和测试结果表明,这种设计有相对较好可拓展性和性能。

同步块与锁的设计

第一、二次作业设计

本实现使用了synchronized关键字标记同步块并实现锁,并测试了使用Synchronized和使用ReetrantLock在性能上的差异。经实验,发现两种机制的性能无较大差异。这主要是因为,在资源竞争不是很激烈的情况下,Synchronized的性能略优于ReetrantLock;但是在资源竞争很激烈的情况下,Synchronized则会较差;而本单元作业即使在极端情形也没有出现非常激烈的资源竞争。

由于在架构上选择了生产者-消费者模式、单级托盘的设计,因此单个请求池RequestPool被设置为唯一的同步对象,该对象的所有public方法均被加锁。请求池不是线程,可以由输入线程写入请求,由电梯线程读出请求。这种方法实现简洁,不容易出现线程安全、死锁等问题。

//RequestPool
	public synchronized void addPersonReq(Person personRequest);

    public synchronized void setEndSign(boolean endSign);

    public synchronized boolean isEnd();

    public synchronized ArrayList<Person> getPersonReq(
            char building, int floor, int dir, int capacity,
            Predicate<Integer> predicate, boolean isVertical, boolean needCheckDir);

    public synchronized boolean hasPersonReq(
            char building, int floor, int dir,
            Predicate<Integer> predicate, boolean isVertical, boolean needCheckDir);

    public synchronized boolean hasReqAhead(
            char building, int floor, int dir, Predicate<Integer> predicate, boolean isVertical);

请求池之外,电梯线程等待的代码块也进行了加锁。

//Elevator
synchronized (requestPool) {
                    if (requestPool.isEnd() && passengers.isEmpty()) {
                        return;
                    }
                    try {
                        requestPool.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }

第三次作业设计

第三次作业在架构上新增了楼层类,在托盘中新增了管理楼层电梯情况的数据结构。因此在共享资源类中,设置了为某一楼层增加横向电梯、获取换乘楼层的方法,这些方法也是被加锁的。

//RequestPool
	public synchronized void addFloorEle(int floor, ElevatorRequest elevatorRequest);

    public synchronized int getTransferFloor(
            int fromFloor, char fromBuilding, int toFloor, char toBuilding);

调度器设计

由于第一、第二次作业可看作第三次作业的子集,这里只说明第三次作业调度的设计和实现。

三级调度机制与实现

  • 对于第三次作业,若要对人请求采用静态拆分,那么我们需要实现的调度机制如下图所示,是一个三级调度机制:首先,在拿到新请求后应拆分请求至相应的换乘楼层(第一级调度);在换乘楼层楼座需要安排所乘电梯(第二级调度);电梯本身的运行策略可看作第三级调度。
graph LR C(1-换乘楼层调度)--换乘楼层-->B(2-楼层电梯调度)--所乘电梯-->D(3-电梯) D--电梯自行运行调度-->D
  • 对于一级调度,采用静态拆分的方法,在人类中增加子请求队列,每完成一个请求就pop(),取队头作为该乘客新的请求;对于二级调度,采用自由竞争的方法,由JVM确定获得该请求的电梯;对于电梯自身运行层面的调度,设置在电梯类内部,电梯每运行一步就调用调度算法确定电梯当前行为。

    graph LR i(personRequest)--break down-->a a(RequestPool)--get transferFloor-->b(floor1)--assign elevator-->c(elevator)-->a c-->c a(RequestPool)--get transferFloor-->bb(floor2)--assign elevator-->c2(elevator)-->a c2-->c2 a(RequestPool)--get transferFloor-->bbb(floor3)--assign elevator-->c3(elevator)-->a c3-->c3

调度算法

  • 确定换乘楼层 确定楼层时,除了基准策略的考虑距离,还需要考虑换乘楼层电梯的忙碌程度和状态。

    • 这里就需要引入代价函数,对于不同的换乘楼层,考虑维护该楼层的侯乘乘客数量,楼层电梯数量,楼层电梯平均速度等方式,计算代价函数:

      \[cost(floor) = \frac {numOfPersonReq(floor)}{\sum_{ele \space \in eles(floor)}{capacity(ele)*speed(ele)}} \]

    • 在选择换成楼层的时候,选择距离+代价最小的楼层:

      \[targetFloor = \arg \min_{floor} [cost(floor) + \lambda \space dist(fromFloor, floor, toFloor)] \]

      其中dist(fromFloor, floor, toFloor)表示纵向从起始楼层=>中专楼层=>目标楼层的距离。

    • 对于上述代价函数中涉及的参数,经实验证明,根据经验调整参数结果可能还不及基准策略;可能的改进是,在随机数据测试的过程中应用模拟退火等优化算法,实现参数调优。(策略本身在不同数据下的表现会出现很大差别,因此优化的目标应该是多次随机数据的测试结果的平均)

  • 单个电梯调度策略

    • 对纵向电梯,利用look算法运行。
    • 对横向电梯,利用改进的als算法运行:在乘客为空的情况下寻找最近的请求(可以掉头),设置为主请求,然后捎带方向相同或不同的乘客(能带就带)。

调度器与的线程交互

  • 对于一级调度,在输入新请求后立即静态拆分。因此不存在与线程的交互。

  • 对于二级调度,在电梯线程竞争RequestPool资源时,由JVM进行公平调度。

  • 对于三级调度,为电梯线程内的方法,决定线程的行为。

    对于三次作业,电梯内部行为都是一致的,电梯线程的生命周期如下图所示:

flowchart LR A(inputHandler)--create-->elevator A-->C subgraph elevator C(endCondition?)--false-->E(has unfinished req?)--false-->E E--true-->F(handle in and out) F-->G(need turn?)--true-->H(dir=-dir)-->GG(move) G--false-->GG-->C end C--true-->D D(end)

架构设计

第一、二次作业

  • 第一,第二次作业的架构设计基本一致,区别仅在于,第二次作业在第一次的基础上增量开发了横向电梯类。

    classDiagram class InputHandler { <<Thread>> -RequestPool requestpool +run() void } class RequestPool { -HashMap<Integer, Person> personPool -boolean endSign +RequestPool() +addPersonReq(Person personRequest) void +setEndSign(boolean endSign) void +isEndSign() boolean +getPersonReq() ArrayList<Person> +hasPersonReq() boolean +hasReqAhead() boolean +sameDir(char fromBuilding, char toBuilding, int dir) boolean } class Elevator{ <<Thread>> -RequestPool requestPool -HashMap<Integer, ArrayList<Person>> passengers +Elevator(RequestPool requestPool, char building, int id, int capacity, double speed) +run() void +move() void +checkDoor() void } class CircElevator{ <<Thread>> -RequestPool requestPool -HashMap<Integer, ArrayList<Person>> passengers +Elevator(RequestPool requestPool, char building, int id, int capacity, double speed) +run() void +move() void +checkDoor() void } class SafeOutput { -long startTimestamp +println(String s) void +initStartTimestamp() void +getRelativeTimestamp(long timestamp) long } class MainClass RequestPool<..InputHandler RequestPool<..Elevator RequestPool<..CircElevator InputHandler<..MainClass Elevator<..MainClass Elevator<..InputHandler CircElevator<..MainClass CircElevator<..InputHandler SafeOutput<..Elevator SafeOutput<..CircElevator

第三次作业

第三次作业在第二次的基础上增量实现了电梯换乘。主要的变化为:(1)对共享资源队列修改:线程结束的条件有所变化;(2)考虑对请求进行拆分,建立每个人的请求队列;(3)电梯运行策略改变,在一个乘客到达目的地的后判断是否还要换乘,换乘则需将请求重新加入请求池。线程安全与调度器设计在前面已经说过。

  • 采用单个请求池,在请求池内增加数据结构管理一个楼层的电梯(包括其停靠楼座等信息)。这里需要注意,访问这一数据结构是需要加锁的。然后可以在请求池中加getTransferFloor方法,以获取换成楼层。

  • 对Person,增设Person类。对拆分后的请求增加每个人的请求队列。换乘的时候,只需进行pop操作即可。

  • 对于进程的结束问题,不能沿用之前的方法。否则会出现有乘客还没完成换乘,就已经结束的情况。为此,可以在请求池中增加计数器PersonReqCnt,只有新增一个乘客是增加,在乘客完全到达最终目的地后再减少。再判断是否结束时只需判断计数器是否为零。

类图如下,

classDiagram class InputHandler { <<Thread>> -RequestPool requestpool +run() void } class RequestPool { -HashMap<Integer, Person> personPool -boolean endSign - private HashMap<Integer, Floor> floorHashMap - private int personCnt +RequestPool() +addPersonReq(Person personRequest) void +addFloorEle(int floor, ElevatorRequest elevatorRequest) void +getTransferFloor(int fromFloor, char fromBuilding, int toFloor, char toBuilding) int +setEndSign(boolean endSign) void +isEndSign() boolean +getPersonReq() ArrayList<Person> +hasPersonReq() boolean +hasReqAhead() boolean -sameDir(char fromBuilding, char toBuilding, int dir) boolean -isReachable(Floor floor, char fromBuilding, char toBuilding) boolean } class Elevator{ <<Thread>> -RequestPool requestPool -HashMap<Integer, ArrayList<Person>> passengers +Elevator(RequestPool requestPool, char building, int id, int capacity, double speed) +run() void +move() void +checkDoor() void } class CircElevator{ <<Thread>> -RequestPool requestPool -HashMap<Integer, ArrayList<Person>> passengers +Elevator(RequestPool requestPool, char building, int id, int capacity, double speed) +run() void +move() void +checkDoor() void } class SafeOutput { -long startTimestamp +println(String s) void +initStartTimestamp() void +getRelativeTimestamp(long timestamp) long } class Person { -ArrayList<PersonRequest> personRequests -boolean needTransfe +Person(PersonRequest personRequest, RequestPool requestPool) +transfer() } class Floor{ -int floor -ArrayList<Integer> masks -int numReq -double cost +addElevator(ElevatorRequest elevatorRequest) void +addReq() void +removeREq() void +getCost() double } class MainClass RequestPool<..InputHandler RequestPool<..Elevator RequestPool<..CircElevator InputHandler<..MainClass Elevator<..MainClass Elevator<..InputHandler CircElevator<..MainClass CircElevator<..InputHandler Person<..RequestPool Person<..Elevator Person<..CircElevator Floor<..RequestPool

时序图如下,

sequenceDiagram autonumber Main->>+InputHandler(Thread): start() Main->>RequestPool: new() Main->>+Elevator(Thread): start() loop till EOF InputHandler(Thread)->>Elevator(Thread): start() InputHandler(Thread)->>RequestPool: add Elevator to certain floor InputHandler(Thread)->>Person: new() InputHandler(Thread)->>RequestPool: addPersonReq() Person ->>+RequestPool: getTransferFloor() RequestPool ->>RequestPool: isReachable() Person ->>Person: transfer() RequestPool -->>-Person: getback end par Elevator run Elevator(Thread)->>+RequestPool: isEnd() RequestPool-->>-Elevator(Thread): isEnd() Elevator(Thread)->>+RequestPool: hasReqAhead() RequestPool-->>-Elevator(Thread): hasReqAhead() Elevator(Thread)->>+RequestPool: hasPersonReq() RequestPool-->>-Elevator(Thread): hasPersonReq() Elevator(Thread)->>+RequestPool: getPersonReq() RequestPool-->>-Elevator(Thread): getPersonReq() Elevator(Thread)->>+Person: tranfer() Person->>+RequestPool: addPersonReq() RequestPool-->>-Person: Person-->>-Elevator(Thread): getPersonReq() Elevator(Thread)->>+SafeOutput: println() SafeOutput->>-Elevator(Thread): end InputHandler(Thread)-->>-Main: end Elevator(Thread)->>-Main: end

BUG分析

  • 自己程序的BUG 三次公测,互测中均未出现bug。

    总结发现自测过程中出现过一个较大的问题:第二次作业横向电梯的调度策略设计不当。

  • 发现BUG的策略

    • 采取的测试策略为大量随机数据自动化评测+少量手造数据的方式

      • 对于第一次作业,编写了自动化测试工具,按照如下所示的pipeline完成一次测试流程:

        graph LR C(Generator)--generate data-->B(Tester)--get result-->A A(Judger)

        其中,评测机支持随机生成与手动输入数据,能够定时定点投喂数据(不用依赖转换程序)。

        经过测试发现一位同学有电梯超载的问题。

      • 后两次作业,因为时间原因,并没有继续维护评测机。选择了和同学share的方法自测,互测主要以手动构造数据为主。

  • 与第一单元的测试机制相比,本单元的评测需要实现定时定点投喂数据,并且在正确性检验上也有所不同。

    数据构造方面,不能一味随机,需要考虑某一座/一层压力较大、某一个方向请求较多等情况增加数据强度。

体验与心得

  • 线程安全和层次化设计
    • 在充分构思好架构的前提下再动手写代码,可以较好的避免出现线程安全问题。有一个简洁的、拓展性强的架构,在后续开发过程中不用经历重构,出现线程安全问题的几率也会下降。
    • 在代码结构上,没有第一单元那样层次分明;但是在设计上依旧体现了层次化。例如,在线程协同的架构模式上体现了层次化;在三级调度架构的设计上体现了层次化。
  • MISC
    • 优化策略上,确定使用一种优化的前提是做好充分扎实的实验,以确保不会出现负优化的情况。
    • 电梯调度显然是一个组合优化问题,但是从本单元的测试结果来看,使用简单的调度算法也可以取得不错的分数。这启发我们在架构与性能的权衡上,更应该注重架构的可拓展性、简洁性、和线程安全。
posted @ 2022-05-04 14:43  gnwekge  阅读(43)  评论(1编辑  收藏  举报