OO_Lab1

问题描述

模拟多线程实时电梯系统,新主楼ABCDE五个楼座各楼层均有电梯,乘客发起形如“从X座x层到Y座y层”的请求,电梯模拟上下行、开关门、乘客进出等行为,以满足所有乘客的要求。

解决思路

各个电梯无论是具体行为还是调度请求都相互独立,因此可以采用多线程的设计思想,每个电梯建立一个线程,这样电梯的行为都可以在线程内完成,可以非常方便的使用 sleep, wait, notifyAll 等函数来模拟消耗时间的操作和等待请求,同时输入也可以采用一个线程来处理。

整体的架构方面,三次作业我都采用了生产者-消费者模型,也就是刚才所提,输入作为生产者写为线程,该线程不断读取输入并提供给托盘;电梯作为消费者处理托盘的输入,每个电梯也写为一个线程;托盘模拟了容纳未处理的请求的容器,供输入线程与电梯线程交互使用。托盘的所有方法全部加锁以防止出现线程安全错误。

由于我在大部分情况下都采用了自由竞争策略,因此不需要显示的设置调度器,而是将请求的分解和分配分别放在输入和电梯线程中,而托盘只设置 addRequest, query, get 等操作。

第五次作业

这次作业较为简单,输入只涉及提供请求,电梯从楼座中选择请求接受,楼座(即托盘)将请求分类存储。

UML类图:

时序图:

后两次作业UML时序图与之相似,不再额外画出。

第六次作业

这次作业新增了横向电梯,但是其实没有什么本质改变,我采用了建立电梯抽象类的方法,仅将少部分横向、纵向电梯不同的方法设置为抽象方法,大部分电梯开关门、上下行、乘客进出对于两种电梯没有区别,可以在抽象电梯类中实现。同时将楼座和楼层抽象为容器(Container)和站点(Station)。对于加入新电梯的请求,只要新开一个对应线程即可。

UML类图:

第七次作业

首先自定义的要求非常容易处理,只要将那些部分从常量变成变量或方法即可。

难点在于请求的分解,我采用了静态分解的方式,也就是在请求被发起时就将其分解为若干横向和纵向请求,因为具体的数据分布无法得知,因此采用静态分配能较大幅度减少编码复杂度。

UML类图:

架构分析

基于度量的分析

对第七次作业进行基于度量的分析(只保留部分):

method CogC ev(G) iv(G) v(G)
code.Elevator.run() 25.0 7.0 10.0 16.0
code.CompoundRequest.solve() 19.0 1.0 12.0 12.0
code.FloorElevator.getNextState(int) 16.0 7.0 7.0 12.0
code.Elevator.checkEnter() 13.0 4.0 6.0 8.0
code.BuildingElevator.getNextState(int) 12.0 9.0 5.0 9.0
code.Elevator.checkExit() 7.0 4.0 4.0 6.0
code.Container.connect(char, char) 6.0 4.0 4.0 6.0
code.PassengerRequester.run() 6.0 3.0 5.0 5.0
code.DirectRequest.DirectRequest(int, int, char, char, Container) 5.0 2.0 2.0 5.0
code.PassengerRequester.addRequest(Request) 5.0 1.0 3.0 3.0
code.Elevator.doorOpen() 4.0 3.0 3.0 5.0
code.Elevator.moveDown() 4.0 2.0 4.0 5.0
code.Elevator.moveUp() 4.0 2.0 4.0 5.0
code.Elevator.passengerExit(CompoundRequest) 4.0 2.0 4.0 5.0
code.Station.getDown(Elevator) 4.0 3.0 4.0 4.0
code.Station.getUp(Elevator) 4.0 3.0 4.0 4.0
code.Station.queryDown(Elevator) 4.0 3.0 3.0 4.0
code.Station.queryUp(Elevator) 4.0 3.0 3.0 4.0
code.Elevator.elevatorEmpty() 3.0 3.0 2.0 3.0
code.Elevator.passengerEnter(CompoundRequest) 3.0 2.0 3.0 4.0
code.Main.main(String[]) 3.0 1.0 4.0 4.0
code.PassengerRequester.addCompoundRequest(CompoundRequest) 3.0 2.0 2.0 3.0
code.CompoundRequest.getFirstRequest() 2.0 2.0 2.0 2.0
code.CompoundRequest.tim(int) 2.0 1.0 1.0 3.0
code.Container.waitForNext() 2.0 1.0 3.0 3.0
code.DirectRequest.buildingName() 2.0 2.0 2.0 3.0
code.DirectRequest.floorName() 2.0 2.0 2.0 3.0
code.DirectRequest.getFromStation() 2.0 2.0 2.0 2.0
code.DirectRequest.getToStation() 2.0 2.0 2.0 2.0
code.DirectRequest.up() 2.0 2.0 1.0 2.0
code.Elevator.doorClose() 2.0 2.0 2.0 3.0
code.Station.addRequest(CompoundRequest, boolean) 2.0 1.0 2.0 2.0
Total 187.0 142.0 176.0 220.0
Average 2.174418 1.651162 2.046511 2.558139

其中复杂度最高的几个方法 Elevator.runElevator.checkEnter()Elevator.checkExit()是电梯运行的核心部分, CompoundRequest.solve(), FloorElevator.getNextState(int), BuildingElevator.getNextState(int) 是电梯调度的核心部分,这两部分决定了大部分电梯运行的主要逻辑,也是我编写代码主要思考的部分。

总的来讲,这些复杂度数值还在可以接受的范围内。

优点

  1. 思路清晰:各个电梯分为各个线程,各个电梯只需处理自己的行为,极大简化了代码编写。电梯、输入、托盘各司其职职责明确,分别提供有限的接口用于线程交互,在编码时不容易出错。各类耦合程度低而内聚程度高。
  2. 抽象程度高:将两类电梯都抽象为电梯实现上下行、开关门、乘客进出;将楼层楼座都抽象为容器和站点的组合,只需使用同一套代码即可解决这两类问题,抽象程度到减少了代码量,也降低了编码和调试难度。
  3. 线程更安全:输入线程与电梯线程、电梯线程之间不直接交互,而是全部通过容器来进行交互,因此只要两类线程都只调用容器的方法,同时对容器加锁,那么就理论上不存在线程安全问题。这可以简化线程的交互,也不用考虑特殊情况是否会出问题。电梯的停止也是由容器来判断。

细节实现

1、复杂请求的分解

如果是纵向请求,则直接加入对应容器,否则选择总距离最短、换乘次数最少、接受任务最少的横向电梯作为中转,代码如下:

public void solve() {
    steps.clear();
    if (this.fromBuilding == this.toBuilding) {
        Container container = containers.get(fromBuilding);
        steps.add(new DirectRequest(fromFloor, toFloor, fromBuilding, toBuilding, container));
        container.addUnfinishedRequest();
    } else {
        int dis = Integer.MAX_VALUE;
        int tim = Integer.MAX_VALUE;
        int unf = Integer.MAX_VALUE;
        int floor = 1;
        for (int i = 1; i <= Main.getFloorCount(); ++i) {
            Container container = containers.get(Main.floorIdToName(i));
            if (container.connect(fromBuilding, toBuilding)) {
                if (dis(i) < dis) {
                    floor = i;
                    dis = dis(i);
                    tim = tim(i);
                    unf = container.getUnfinishedRequest();
                } else if (dis(i) == dis && tim(i) < tim) {
                    floor = i;
                    tim = tim(i);
                    unf = container.getUnfinishedRequest();
                } else if (dis(i) == dis && tim(i) == tim && container.getUnfinishedRequest() < unf) {
                    floor = i;
                    unf = container.getUnfinishedRequest();
                }
            }
        }
        if (this.fromFloor != floor) {
            Container container1 = containers.get(fromBuilding);
            steps.add(new DirectRequest(fromFloor, floor, fromBuilding, fromBuilding, container1));
            container1.addUnfinishedRequest();
        }
        Container container2 = containers.get(Main.floorIdToName(floor));
        steps.add(new DirectRequest(floor, floor, fromBuilding, toBuilding, container2));
        container2.addUnfinishedRequest();
        if (floor != this.toFloor) {
            Container container3 = containers.get(toBuilding);
            steps.add(new DirectRequest(floor, toFloor, toBuilding, toBuilding, container3));
            container3.addUnfinishedRequest();
        }
    }
}

2、线程的结束

线程的结束可以理解为特殊的请求,即输入线程请求结束整个程序,然后由容器(托盘)向电梯发出停止的请求,电梯在执行完所有请求后结束自身。

PassengerRequester:

for (Character key : containers.keySet()) {
    containers.get(key).requestTerminate();
}

Container:

private void checkStop() {
    if (stop()) {
        notifyAll();
    }
}

public synchronized boolean stop() {
    return requestTerminate && unfinishedRequest == 0;
}

public synchronized void requestTerminate() {
    requestTerminate = true;
    checkStop();
}

Elevator:

private boolean elevatorEmpty() {
    for (Queue<CompoundRequest> q : this.acceptedRequests) {
        if (!q.isEmpty()) {
            return false;
        }
    }
    return true;
}

private boolean checkStop() {
    return container.stop() && elevatorEmpty();
}

3、线程通信

采用 wait-notify 机制,在 Container 里进行通信:

public synchronized void addRequest(CompoundRequest compoundRequest) {
     stations.get(compoundRequest.firstRequestFromStation())
         .addRequest(compoundRequest,compoundRequest.firstRequestDirection());
    notifyAll();
}

public synchronized void waitForNext() {
    try {
        if (!stop()) {
            wait();
        }
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}

性能策略

1、电梯的请求查询

对于每个楼层、楼座,将所有请求按每层上下分到各个队列里。电梯使用 getNextState 来接收新请求。对于纵向电梯,优先选取在其上方向上的请求,对于横向电梯,优先选取离其最近的请求。

2、横向请求方向

我采用了将所有请求视为同一方向的策略。

3、多个电梯任务分配

我采用了自由竞争的方式,即各个电梯同时去找各个请求,先到先得,这样在简化代码难度的情况下也能保证不错的性能。

\[bug惩罚分=未被修复的bug数\times\alpha+非合并修复对应的bug数+合并修复次数,\quad其中\alpha=2 \]

代码测试

我采用了随机生成数据与手动测试相结合的方式,通过随机大量数据来覆盖大部分错误,同时手动针对电梯超载、在错误楼层停下等bug进行测试。

发现的bug

1、多线程下,不能错误的认为已经判断过的情况接下来不会再发生。

我使用了 checkEnter 函数判断了是否有乘客进入,接下来错误的认为只有电梯移动和空等的可能,但是实际上可能在 checkEnter 后出现请求,使得我的电梯错误运行。

 posted on 2022-04-30 10:47  15101051  阅读(39)  评论(1编辑  收藏  举报