面向电梯设计与构造——第二单元总结

符悦-21231042 学生 2024-04-19 14:16:27

本篇总结很长,有很多平淡无趣的部分,推荐阅读的部分为

  • 为什么电梯要继承
  • 销毁大于转化
  • 分配策略——超越影子电梯

架构分析

img

架构图如上,大概分为以下几个部分

  • Threads:专门的线程类,继承Thread,实现了run方法
  • Controllers:电梯的控制器,实现了电梯的运行逻辑和调度管理,实现了Contoller接口,ControllerManager管理所有的Controller
  • Elevator:电梯类,实现了电梯的运行行为
  • Utils:工具类(包装工具函数),辅助元组,乘客类( 包装Request)
  • Main:程序入口

架构理念

本次的架构我认为设计的还是非常优秀的,架构设计的理念主要是

  • 行为、逻辑和线程分离
  • 适当的继承保证抽象性

行为、逻辑和线程分离

在本次作业中,加入了多线程的概念,在设计的开始,我们就应该思考,应该将哪个部分作为线程,在这个问题下,一般有以下三种实践:

  • 电梯直接继承线程
  • 实现一个控制类,控制电梯并继承线程
  • 单独做一个线程类

其中,第一种实践是最差的,会导致电梯的代码包含了控制逻辑,并且在第七次作业的迭代中导致电梯难以被继承(大量冗杂的代码都需要重写)。

简单的来说,第一种方式的错误在于电梯不是线程

电梯不是线程

电梯就是电梯,除了上下开关不应该有其他功能

为了搞清楚是否需要单独实现线程类,我们首先要想清楚,线程中需要做什么

简单来说,线程做了以下三个任务:

  • 循环执行运行逻辑
  • 在合适的时机退出
  • 如果没有任务,那么等待

如下代码所示,线程的run方法实际上是公式化的

// run from ControlThread
public void run() {
        while (true) {
                // check wait condition
            if (autoController.noRequest() && elevator.isEmpty()
                    && !autoController.inReset() && !autoController.ended()) {
                synchronized (autoController) {
                    try {
                        autoController.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
                        // check exit condition
            if (autoController.ended()) {
                try {
                    elevator.close();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                break;

            }
                        // control elevator
            autoController.elevatorControl();
        }
    }

以上是普通电梯运行的线程代码,循环中代码基本分为三个部分

  • autoContoller获取电梯运行状态,检查是否需要等待
  • 检查是否需要退出线程
  • 控制电梯

实际上,2、3两种实现都是可用的,我选择了单独实现线程类略微降低了代码的耦合,但是会导致多出来多个单独的线程类,本着线程类的作用和Main的作用同等的设计理念,建议采用方法3

适当的继承保证抽象性

这里主要涉及第七次作业的迭代,在我的架构中,基本上存在两个部分的继承

  • 双轿厢电梯继承自普通电梯,增加了变量纪录层数限制和轿厢类别(AB),重写了少数方法
  • 双轿厢控制器继承自普通电梯控制器,重写了得分(这是之后要用到的妙妙工具)和运行逻辑
  • 控制器类实现Contoller接口,保证不同控制类在ContollerManager眼中是均等的

电梯为什么要继承

其实不不继承问题也不大(因为电梯中没有运行逻辑),这里涉及一个小的trick

相信大多数同学在第七次作业中,都重新判断了电梯类型,然后分开输出普通电梯的输出和双轿厢电梯AB电梯的输出,然而有一个方便的方法可以简化代码

  1. 在普通电梯类中实现toString,输出电梯号,调用toString输出

    // just print arrive at current level
        public synchronized void arrive() {
            TimableOutput.println(
                    "ARRIVE-" + curLevel + "-" + toString()
            );
        }
       
    @Override
    public String toString() {
            return Integer.toString(id);
        }
    
  2. 双轿厢电梯类中重写toString,输出电梯号-电梯类型

    @Override
        public String toString() {
            return super.getId() + "-" + label;
        }
    

奇妙的事情发生了,当我们调用双轿厢电梯中继承的方法时,输出的信息会携带label!

为什么双轿厢电梯控制器继承了普通控制器,还要设计一个接口

因为双轿厢电梯不是一个电梯,而是两个电梯

所以继承的控制器实际上是每个电梯的控制器,我还实现了一个总控制器,它运行在单独的线程中,调度AB电梯的请求和控制中间层的交互

而在ContollerManager(负责分配请求和结束线程)眼中看来,双轿厢总控和普通控制器是一样的

public interface Controller {
    boolean inReset();
    void resetElevator(int capacity, double moveTime, boolean dc, int level);
    void exit();
    boolean noRequest();
    void addRequest(Passenger passenger);
    boolean overload();
    double requestScore(Passenger passenger);
    boolean elevatorEmpty();
}

线程通信

终于到了这个作业中最麻烦的地方,为了设计线程之间的交互,首先看看在我的设计中,线程间的关系如何

img

  1. Main产生输入线程和管理线程,输入线程负责处理输入,并且产生请求,送到管理线程,输入结束后输入线程就会退出
  2. 管理线程产生控制线程,管理器ContollerManager从输入线程接受请求,并且分配给其他Contoller
  3. 对于双轿厢电梯,DcControlThread负责接受请求,再分配给DcSubControlThread中运行的DcAutoContollerDcContoller负责和ContollerManager通信,DcAutoContoller负责控制电梯的行为

如何避免竞争和死锁

其实就两个点

  • 读写会被多个对象调用的数据时,上锁
  • 尽量控制同步块区域,不要滥用同步块

请求的交互和双轿厢电梯——销毁大于转化

本次作业的所有更换行为(reset送回请求和重置双轿厢电梯),都推荐直接销毁旧的乘客、电梯,换成新的

reset送回请求

此处需要修改请求的From,容易忘,直接销毁乘客创建新的就好

重置双轿厢电梯

if (dcReset) {
            exit();
            DcController dcController 
                            = new DcController(id, controllerManager, resetSwitchLevel);
            DcElevator upperElevator
                    = new DcElevator(id, Elevator.getMaxLevel(), resetSwitchLevel, "B");
            upperElevator.set(resetCapacity, resetMoveTime);
            DcElevator lowerElevator = new DcElevator(id, resetSwitchLevel, 1, "A");
            lowerElevator.set(resetCapacity, resetMoveTime);
            dcController.setUpperElevator(upperElevator);
            dcController.setLowerElevator(lowerElevator);
            DcContolThread dcContolThread 
                            = new DcContolThread(dcController, controllerManager);
            dcContolThread.start();
            controllerManager.removeController(id);
            controllerManager.addController(id, dcController);
        }
  1. 退出当前控制线程
  2. 产生所有双轿厢控制线程所需的对象
  3. 把双轿厢控制器加入ContollerManager,并移除自身
  4. 启动双轿厢控制线程

双轿厢不撞问题

挺好解决的,每个轿厢试图进入交换楼层时会有以下行为

  1. 向总控制器发出请求,查询总控制器是否记录了交换楼层已经有电梯,如果有,等待
  2. 进入交换楼层,总控制器中将交换楼层标记为已经占用
  3. 开关,放人接人等行为
  4. 无论是否有请求,电梯是否有乘客,离开交换楼层,并且通知总控制器,总控制器将交换楼层标记为空闲
  5. 如果有等待的轿厢,总控制器通知其进入交换楼层
@Override
    protected void transport() {
        Passenger passengerAtCurLevel;
        // fetch passengers in current level
        while (true) {
            if (getElevator().isFull()) {
                break;
            }
            passengerAtCurLevel = getRequest(getElevator().getLevel());
            if (passengerAtCurLevel == null) {
                break;
            }
            try {
                getElevator().open();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            getElevator().addPassenger(passengerAtCurLevel);
        }
        try {
            getElevator().close();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        setMoveDir();
        // set flags
        boolean releaseFlag = false;
        if (inSwitchFlag) {
            inSwitchFlag = false;
            releaseFlag = true;
        }
        move();
        if (releaseFlag) {
            dcController.releaseSwitch();
        }
        ArrayList<Passenger> passengers = null;
        if (getElevator().hasOutPassenger()) {
            try {
                getElevator().open();
                passengers = getElevator().dropAllPassengers();
                getElevator().close();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        if (inSwitchFlag) {
            requirement = false;
            if (passengers != null) {
                sendRequestToSwitch(passengers);
            }
        }
    }

调度策略——如何超越影子电梯

叠个甲,hw6强测99.79,hw7强测99.93

首先,你的轿厢应该LOOK

对于双轿厢,我采用的LOOK算法会永远把上/下界限定为换乘层(这不一定有益于性能)

什么是好的调度策略

实际上好的调度策略只需要满足两点

  • 同样的性能条件下,尽量平均分配(如果需要分配四个一样的请求,有两个完全性能一致,接受请求数一样,电梯内乘客数一样,所在层和移动方向一样的电梯,那么这四个请求应该被2-2分配)
  • 同样的载重情况下,尽量将请求分配给性能更好的电梯(如果有一个请求,有两个空的电梯,那么这个请求应该被分配给更快的电梯)

从这两个理念出发,我们来看看现有的几个分配策略

常用分配策略

  • 顺序分配:从1开始轮流分配,绝对的平均,没时间写分配策略的结果
  • 随机分配:用随机数指定分配电梯,在大量数据,平均性能下能达到最好,属于不需要花时间,但是性能尚可的分配策略,可以被看作基准
  • 得分分配:考虑电梯的当前状态参数,建立一个数学模型,计算出得分,我所采用的方法
  • 影子电梯:完全模拟每个电梯接收请求的结果,选择最短时间,高端分配策略的代表,公认的最优性能

研讨中提到得分分配的同学均认为得分分配性能不如影子电梯,但是实际上我所采用的得分分配比同组采用影子电梯的同学性能略好,恐怕是建模方式不同导致的

要想超越影子电梯,首先我们来想一想,影子电梯的缺陷在哪里

为什么影子电梯打不过自由竞争

  • 自由竞争是动态分配,防止了电梯的木桶效应

    • 这是本学期的分配要求下无法做到的,本学期的分配由于RECEIVE约束,只能是静态分配,因此一定会产生木桶效应,即由于多线程程序的随机性和数据的随机性,最终容易产生少数电梯仍然存在请求,多数电梯请求已经完成的情况

      一梯有难,五梯围观

  • 影子电梯产生的是局部最优解,无法预知未来

    • 影子电梯的计算无法考虑到未来的请求对过去的请求产生的影响,即当新的请求被加入时,实际上之前请求的计算不再有效(当前请求会对之前请求的时间产生惩罚)

建模得分

先上实现

ControllerManager中的分配方法

分数最低的为分配电梯(这个意义上,这可能更应该叫做预估时间或者惩罚值)

// acquire controller id by distribute algorithm
    // return -1 if there is no available elevator
    private int distributeElevator(Passenger passenger) {
        int controllerId = -1;
        double score = 1e6;
        for (int key : autoControllers.keySet()) {
            Controller autoController = autoControllers.get(key);
            // if elevator is in reset or elevator is overload
            // (all request received > 1.5 * elevator capacity)
            // pass
            if (autoController.inReset() || autoController.overload()) {
                continue;
            }
            double controllerScore = autoController.requestScore(passenger);
            if (controllerScore < score) {
                score = controllerScore;
                controllerId = key;
            }
        }

        return controllerId;
    }
  • 普通电梯

    public synchronized double requestScore(Passenger passenger) {
            double t1 = requestFromPassengerTime(passenger);
            double t2 = requestTargetPassengerTime(passenger);
            double c = elevator.getPassengerCount();
            double m = elevator.getCapacity();
            double r = requestSize();
            double overload = c + r - m > 0 ? c + r - m : 0;
            return t1 + 0.5 * (t2 - t1) + 0.4 * (c + r) + 1.5 * overload;
        }
    
    • t1:电梯从当前层运行到请求出发楼层的直线时间(只考虑方向,楼层限制和速度,不考虑上下乘客)
    • t2:电梯从当前层运行到请求到达楼层的直线时间(同样)
    • c:电梯内乘客数
    • m:电梯最大载重量
    • r:当前已分配,未进入电梯请求数

    分数的构成分为三个部分

    • 运行时间的预估值t1 + 0.5 * (t2 - t1)
    • 对已有请求的罚时0.4 * (c + r)
    • 超重罚时1.5 * overload

    其中,magic number均为参数,可以调整,这是简单实验了几组最终选择的参数

  • 双轿厢电梯

    双轿厢电梯由于电量优势和吞吐量优势,请求会更可能被分配给双轿厢电梯

    // final score from DcController
    @Override
        public synchronized double requestScore(Passenger passenger) {
            boolean onlyLower
                    = passenger.getFromLevel() <= switchLevel
                    && passenger.getTargetLevel() <= switchLevel;
            double upperScore = upperController.requestScore(passenger);
            boolean onlyUpper
                    = passenger.getFromLevel() >= switchLevel
                    && passenger.getTargetLevel() >= switchLevel;
            double lowerScore = lowerController.requestScore(passenger);
            if (onlyUpper) {
                return upperScore;
            }
            if (onlyLower) {
                return lowerScore;
            }
            return upperScore + lowerScore;
        }
        
    // score of each elevator from DcAutoController
    @Override
        public synchronized double requestScore(Passenger passenger) {
            // doesn't switch
            if (label == Label.Up) {
                if (passenger.getFromLevel() >= getElevator().getMidLevel()
                        && passenger.getTargetLevel() >= getElevator().getMidLevel()) {
                    return super.requestScore(passenger) * 0.8;
                }
            } else {
                if (passenger.getFromLevel() <= getElevator().getMidLevel()
                        && passenger.getTargetLevel() <= getElevator().getMidLevel()) {
                    return super.requestScore(passenger) * 0.8;
                }
            }
            double c = getElevator().getPassengerCount();
            double m = getElevator().getCapacity();
            double r = requestSize();
            double overload = c + r - m > 0 ? c + r - m : 0;
            double requestTime;
            // require switch
            if (label == Label.Up) {
                if (passenger.getFromLevel() > getElevator().getMidLevel()) {
                    requestTime = requestTime(passenger.getFromLevel(),
                            getElevator().getMidLevel(),
                            getMoveDir(), upperLimit(),
                            lowerLimit(), getElevator().getMoveTime());
                } else {
                    requestTime = requestTime(getElevator().getMidLevel(),
                            passenger.getTargetLevel(),
                            getMoveDir(), upperLimit(),
                            lowerLimit(), getElevator().getMoveTime());
                }
            } else {
                if (passenger.getFromLevel() < getElevator().getMidLevel()) {
                    requestTime = requestTime(passenger.getFromLevel(),
                            getElevator().getMidLevel(),
                            getMoveDir(), upperLimit(),
                            lowerLimit(), getElevator().getMoveTime());
                } else {
                    requestTime = requestTime(getElevator().getMidLevel(),
                            passenger.getTargetLevel(),
                            getMoveDir(), upperLimit(),
                            lowerLimit(), getElevator().getMoveTime());
                }
            }
            return 0.8 * requestTime + 0.4 * (c + r) + overload;
        }
    

    双轿厢电梯的分数由总控制器提供给ControllerManager,分数的构成由每个电梯的分控制器提供

    • 如果请求不涉及换乘,返回分区分控制器的分数
    • 如果涉及换乘,返回两个分控制器分数和

    分控制器的分数为

    • 不涉及换乘,等同于普通电梯计算算法的0.8倍
    • 涉及换乘,等同于从出发楼层到换乘楼层/换乘楼层到目标楼层的直线时间*0.8 + 其他罚时

    这个部分当时比较懒,没有认真设计,不过最后结果证明效果不错

可以看到,关键的地方在于,不仅要预估到达时间,还要考虑载重和已有请求产生的罚时

最后的模型如下

$$ score = \alpha \cdot directTime + \beta \cdot requestPanalty + \gamma \cdot overloadPanalty $$

优点

  • 最优性能,由于多线程随机性产生的木板效应,实际上并不是100的测试点大抵是随机性导致的,这个分配算法应该是最优性能(当然实现好的影子电梯也能达到最优性能)
  • 计算简单,比起影子电梯,产生bug概率低得多,并且所有值的计算均为线性时间求解,计算比影子电梯快得多

调参

说实话,没那么大必要,随便调一下就行,对性能的影响没那么大

如果真的要最优解,可以考虑启发式算法+随机数据模拟跑个一万组

抛砖引玉

还有两种分配策略恐怕可以达到最优性能

  • 强化学习(也不是不行,毕竟电梯的运行类似马尔可夫过程)
  • 带权随机
    • 类似得分,用已有信息建立权重模型,然后计算带权随机
    • 权重函数应该是已有信息的非线性函数,保证在特别情况下能从随机退化为几乎必中

强化学习是说笑的,带权随机真的可以尝试一下

bug分析

本次作业最容易出bug的地方就是各种各样的条件判断中漏项导致的bug

如果作业的实现合理,不滥用同步块,那么死锁问题应该并不容易遇到(我只遇到过一次,是由于滥用同步块导致的,直接重构)

然而条件判断......每次互测都死在这个bug上。

 // a part of thread run()
 // 4 different conditions is needed here
 if (autoController.noRequest() && elevator.isEmpty()
                    && !autoController.inReset() && !autoController.ended()) {
                synchronized (autoController) {
                    try {
                        autoController.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
  
// a method in ContollerManager
// method for condition check
public synchronized boolean ended() {
        if (exit) {
            return true;
        }
        if (inputFinish && allControllersNoRequest()
                && allElevatorsEmpty() && noRequest()
                && !anyElevatorInReset()) {
            return true;
        }
        return false;
    }

如上所示,很多地方需要4个甚至5个条件判断,有的部分甚至需要把条件做二次抽象

心得

比上一次作业写得爽,果然合理的架构是关键

从单线程到多线程的过程并不是一番通顺,确实hw5-7相较于之前有难度上的跃升。不过只要架构合理,迭代就不会特别痛苦

期待下一次作业

...全文
78 回复 打赏 收藏 转发到动态 举报
写回复
用AI写文章
回复
切换为时间正序
请发表友善的回复…
发表回复

301

社区成员

发帖
与我相关
我的任务
社区描述
2023年北航面向对象设计与构造
学习 高校
社区管理员
  • YannaZhang
  • CajZella
  • C_ecelia
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告
暂无公告

试试用AI创作助手写篇文章吧