2024OO第二单元:电梯调度策略

本文详细描述了通过多线程模拟电梯运行的三次编程作业,涉及线程同步、生产者-消费者模型、锁的设置、策略类设计以及调度策略的改进,包括LOOK算法和到达时间最短策略。作者分享了遇到的问题、解决方案和学习心得。
摘要由CSDN通过智能技术生成

前言

第二单元的主题是多线程,具体要求为模拟运行新主楼的电梯,本单元三次作业均围绕电梯的运行、接客、结束等操作,运用多线程模拟多部电梯,并使用适当的运行策略调度策略来提高电梯的性能。

由于之前并没有接触过多线程,导致在作业完成过程中出现了许多线程不安全的问题,我本单元的成绩也相比第一单元低了很多(o(╥﹏╥)o),但也从中学到了生产者-消费者设计模式,对同步块的理解也进一步加深,下面将先分析三次作业的架构,然后再分析多线程中锁与同步快的关系,最后是自己出现的bug以及心得体会。

第一次作业

第一次作业相对比较简单,虽然是多线程,但在输入时已经指定了乘客乘坐的电梯,我们只需要实现电梯类并开启6个电梯线程并可以解决该问题,主要难点在于如何将乘客请求分配给指定的电梯电梯的运行策略是什么,如何实现捎带

UML图

在这里插入图片描述

时序图

在这里插入图片描述

生产者-消费者模式

对于该问题,我们可以抽象为生产者-消费者问题,其中电梯为消费者,电梯分配器(Schedule类)为生产者,Schedule将乘客请求通过托盘RequestTable类分给电梯,电梯进行执行,其结构如下:

  • 生产者:Schedule类,生产产品为乘客请求
  • 消费者:Elevator类,对分发乘客请求进行响应
  • 托盘:RequestTable类,主要为一个请求列表

其结构如图所示:
在这里插入图片描述

那问题来了,为何不直接令InputHandle类为生产者

选择Schedule类主要为了之后作业中乘客不指定电梯,由我们自己决定哪个电梯更适合接该乘客,可扩展性更高

锁的设置

本次作业的锁设置在RequestTable类中,因InputHandle、Schedule和Elevator均共享该资源,特别是Schedule类和电梯类会共享同一RequestTable类,因此需在该类中加入请求和删除请求等方法加锁,具体实现如下:

public class RequestTable {
    private final ArrayList<PersonRequest> table;
    private int requestNum;
    private int end;
    
    public synchronized void addRequest(PersonRequest request) {...}
    
    public synchronized void delRequest(PersonRequest request) {...}
    
    ...

以此来保证只有一个线程对资源进行访问。

策略类

运行策略

电梯捎带策略是一个没有最优解的问题,每个算法都有使其性能很差的情况,我们可以采用ALS,LOOK等策略,这里参考hyggge学长的博客,采用LOOK算法实现电梯的捎带策略:

LOOK

算法如下:

  • 首先为电梯规定一个初始方向,然后电梯开始沿着该方向运动。

  • 到达某楼层时,

    首先判断是否需要开门

    • 如果发现电梯里有人可以出电梯(到达目的地),则开门让乘客出去;
    • 如果发现该楼层中有人想上电梯,并且目的地方向和电梯方向相同,则开门让这个乘客进入。
  • 接下来,进一步判断电梯里是否有人

    。如果电梯里还有人,则沿着当前方向移动到下一层。否则,检查请求队列中是否还有请求(目前其他楼层是否有乘客想要进电梯)——

    • 如果请求队列不为空,且某请求的发出地是电梯"前方"的某楼层,则电梯继续沿着原来的方向运动。

    • 如果请求队列不为空,且所有请求的发出地都在电梯"后方"的楼层上,或者是在该楼层有请求但是这个请求的目的地在电梯后方(因为电梯不会开门接反方向的请求),则电梯掉头并进入"判断是否需要开门"的步骤(循环实现)。

    • 如果请求队列为空,且输入线程没有结束(即没有输入文件结束符),则电梯停在该楼层等待请求输入(wait)。

    • 注意:电梯等待时运行方向不变。在我的设计中,运行方向是电梯的一个状态量而不是过程量,用来表示下一次move时的方向。当有新请求进入请求队列时,电梯被唤醒,此时电梯的运行方向仍然是电梯wait前的方向。

    • 如果请求队列为空,且输入线程已经结束,则电梯线程结束。

策略类的设置

这里参考hyggge学长的博客,将电梯类与策略类进行分离,策略对请求列表和电梯位置进行分析,给出电梯下一步应该开关门、调转、移动和结束等操作,策略通过Advice枚举类中的MOVE,OPEN,WAIT,STOP,TURN进行返回,使代码可读性更高。

public class Strategy {
    private final RequestTable table;

    public Advice getAdvice(int direction, int position, HashMap<Integer,
            ArrayList<Person>> handleList, int nowNum) {
        if (nowNum != 0 && handleList.containsKey(position)) { //可以出
            ...
        }
        if (nowNum < 6) {
            for (int count = 0; count < table.getRequestNum(); count++) {...} //可以进
                
        }
        if (nowNum != 0) { //继续运
            ...
        }
        if (table.ifEmpty()) { //无请求
            ...
        }
        for (int count = 0; count < table.getRequestNum(); count++) { //继续还是掉头
           ... //判断
        }
        return Advice.TURN;
    }
}

Strategy类在电梯类中,调用上面的getAdvice()方法获得建议。

public void run() {
        while (true) {
            Advice advice = strategy.getAdvice(direction, position, handleList, numPer);
            if (advice == Advice.MOVE) {
                this.move();
            } else if (advice == Advice.OPEN) {
                this.open();
                this.out();
                this.in();
                this.close();
            } else if (advice == Advice.TURN) {
                this.direction = -1 * this.direction;
            } else if (advice == Advice.WAIT) {
                this.table.waitRequest();
            } else if (advice == Advice.STOP) {
                break;
            }
        }
    }

复杂度分析

本次作业代码复杂度如下:

在这里插入图片描述

本次作业中代码复杂度相对较低,其中Stragety.getAdvice()Elevator.in()方法复杂度较高,主要因为Stragety类需要进行电梯相关位置信息的分析,需进行大量的判断,以及电梯in()方法需要判断是否有人进入,导致复杂度较高。

第二次作业分析

本次作业的乘客请求不再指定电梯,需要我们自行设计调度策略,同时加入了电梯RESET要求,电梯可能在运行过程中会重置电梯参数,同时,本次作业不支持自由竞争,引入了RECEIVE约束。

UML图

在这里插入图片描述

时序图

在这里插入图片描述

调度策略

本次作业便可用到上次作业中的Schedule类,这里可以使用很多调度策略,如随机分配,顺序分配,请求人数最少、调参、影子电梯等方法,这里若想了解更多,可参考Thysrael 学长的博客,我的调度策略为到达时间最短策略,类似于平常生活中的电梯策略,即计算出每部电梯到达乘客请求楼层的时间,其中包括移动时间,开关门时间等,选择其中时间最短的电梯,保证乘客能尽快乘上电梯。过程如下:

  • 电梯已经超载,给时间加上一个很大的数(从而可以分给其他电梯,若都超载,则都加上相同的数,无影响)
  • 若正在重置,计算出电梯结束重置所需要的时间(在重置时会获取电梯的系统时间)
  • 计算需要跨越几个楼层接到乘客,计算其移动时间。
  • 若在这些楼层有乘客会进入或退出,则需加入开关门时间。
  • 计算完时间之后,若几部电梯时间相等或相差不大(100ms以内)则在这几部电梯中随机选择。(防止默认都分给一部电梯)

代码结构如下:

public int getTime(int from, int to) {
        int time = 0;
        if (numPer + table.getRequestNum() + waitQueue.getRequestNum() > maxNum) {
            time += 4000;
        }
        if (state == 2) { //正在重置
            long currentTime = System.currentTimeMillis();
            time += (int) (1200 - (currentTime - beginResetTime));
        }
        if ((state == 1 && Math.abs(from - position) > resetCount)) { //即将重置
            time += 1200;
        }
        if ((from - position) * direction > 0) { //同方向
            time += (int) (moveTime * 1000) * Math.abs(from - position);
            ... //计算开关门时间
        } else if ((from - position) * direction < 0) {
            int limited = getLimitPos();
            time += (int) (moveTime * 1000) * (Math.abs(limited - position) * 2
                    + Math.abs(position - from));
            ... //计算开关门时间
        }
        return time;
    }

RESET请求

电梯在运行一段时间以后,其性能参数(满载人数移动时间)可能会发生改变。接收到重置指令的电梯需要尽快停靠后完成重置动作,再投入电梯系统运行。为安全起见,电梯重置时内部不可以有乘客,且重置动作需要1.2s。

这里只列举几个多线程中容易出现的bug以及解决措施:

  • RESET和RECEIVE同时输出:

    作业规定RESET_BEGIN之后将不能再RECEIVE乘客,但有可能会出现以下情况:在这里插入图片描述

    这是因为我们设置电梯状态为重置状态之后还未来得及同步给Schedule类的间隙时间内,来了一个请求分给了该电梯,因此,我选择在设置电梯为重置状态(此时电梯的请求会被加入等待队列不输出RECEIVE),先睡50ms,在输出RESET_BEGIN,即:

    public void reset() {
            ...
            state = 2; //设置为重置状态
            try {
                sleep(50);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            TimableOutput.println("RESET_BEGIN-" + this.id);
            try {
                sleep(1200);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        	...
    }
    
  • RESET之后移动了三次

    这也是多线程同步的问题,虽然我加入了计数器,只让电梯在RESET之后只移动两层,但很可能在设置计数器的间隙内,便有时间片输出了ARRIVE,此时计数器未来得及计数,从而会移动三层。因此, 我选择在重置之后只尽力移动一层,保证线程安全。

锁的设置

本次作业锁的设置依然在RequestTable类中,与上次作业并无差别。

复杂度分析

在这里插入图片描述

与上次作业相同,代码复杂度明显增高,因增加了RESET请求,需要增加大量判断代码以及对Elevator类的修改,并且将调度策略中getTime()方法放在Elevator类中,导致Elevator类复杂度较高,方法复杂度较高的也都集中在StragetyElevator类中。

第三次作业

本次作业增加了第二类重置要求,电梯可以变为双轿厢电梯,分为上(B)下(A)两部,中间有换乘楼层。难点主要在于架构如何实现双轿厢电梯如何避免双轿厢电梯相撞以及调度策略的变化。

UML图

在这里插入图片描述

时序图

在这里插入图片描述

架构实现

在思索良久之后,决定在原有结构中进行扩展,而非重构,我在原来的单轿厢电梯中加入ElevatorController,该类起到二次分配器的作用,若电梯为单轿厢,则其为null,否则,它会在构建时创建两个elevator类包含在其中,从而实现双轿厢电梯的架构,代码和架构设计图如下:

public class EleController extends Thread {
    private final RequestTable mainRequest;
    private final Elevator elevatorA;
    private final Elevator elevatorB;
    private final int transferFloor;

    public EleController(int id, int transferFloor, int maxNum,
                         double moveTime) {
        mainRequest = new RequestTable();
        ...
        elevatorA = new Elevator(id, tableA, "A", maxNum, moveTime, transferFloor,
                this);
        elevatorB = new Elevator(id, tableB, "B", maxNum, moveTime, transferFloor,
                this);
        this.transferFloor = transferFloor;
    }
    ...
}

在这里插入图片描述

因此我们便实现了双轿厢电梯的架构。

策略类调整

在如何防止双轿厢电梯相撞的问题上,可以选择通过设置一个标志变量,然后通过获得该变量的锁来实现,也可以通过二次分配器来获取搭档电梯的相关信息来进行分类讨论,这里我选择后者:

public Advice getAdviceDouble(int direction, int position, HashMap<Integer,
            ArrayList<Person>> handleList, int nowNum, int max) {
        ...
        if (nowNum != 0 && handleList.containsKey(position)) { //可以出
            ...
        }
        if (nowNum < max) {
            for (int count = 0; count < table.getRequestNum(); count++) { //可以进
               ...
            }
        }
        if (nowNum != 0) { //继续运
            if (name.equals("A") && position == transfer - 1 && direction == 1) {
                return ifMoveWait(posAno, directionAno, transfer);
            }
            if (name.equals("B") && position == transfer + 1 && direction == -1) {
                return ifMoveWait(posAno, directionAno, transfer);
            }
            return Advice.MOVE;
        }
        if (table.ifEmpty()) { //无请求
            ...
        }
        for (int count = 0; count < table.getRequestNum(); count++) { //继续还是掉头
            PersonRequest request = table.getPerRequest(count);
            int from = request.getFromFloor();
            if (((from - position) * direction > 0)) {
                if (from == transfer) {
                    return noPersonButTableHave(transfer, position, posAno,
                            direction, directionAno);
                }
                return Advice.MOVE;
            }
        }
        return Advice.TURN;
    }

大概思路如下:

  • 某一电梯到达换成楼层上(或下)一层,判断搭档电梯是否在换乘楼层,是则GAP(即睡100ms,等待搭档电梯离开)
  • 若搭档电梯也在楼层上(或下)一层并且方向也指向换乘楼层,若搭档电梯已经开始移动,则GAP,若未开始移动,若本电梯是A电梯,则GAP,是B电梯,则MOVE,即B的移动优先级大于A。

注意:若搭档电梯在换乘楼层上(或下)一层但方向并不指向换乘楼层,则该电梯可以自由移动。

调度策略

由于hw6中我使用了到达时间最短的调度策略,因此在hw7也采用该策略,对于双轿厢电梯,若需要换乘,则计算两部电梯的到达时间(一个到出发楼层,一个到换乘楼层),若不需要换乘,计算其所搭乘的电梯时间即可。

//EleController.java
public int getTime(int from, int to) {
        int time = 0;
        if ((from - transferFloor) * (to - transferFloor) < 0) {
            if (from > to) {
                time += elevatorA.getTime(transferFloor, to);
                time += elevatorB.getTime(from, transferFloor);
            } else {
                time += elevatorA.getTime(from, transferFloor);
                time += elevatorB.getTime(transferFloor, to);
            }
        } else {
            if (from < transferFloor) {
                time += elevatorA.getTime(from, to);
            } else if (from > transferFloor) {
                time += elevatorB.getTime(from, to);
            } else {
                if (from < to) {
                    time += elevatorB.getTime(from, to);
                } else {
                    time += elevatorA.getTime(from, to);
                }
            }
        }
        return time;
    }

复杂度分析

在这里插入图片描述

与上次作业相比,加入了双轿厢电梯,但总体上代码复杂度并无较大变化,主要因为新建了类,更好的进行封装。

bug分析

hw5

hw5强测中出现了RTLE的情况,助教说可能是死锁问题,但第五次作业的线程同步问题并没有那么复杂,我经过检查代码逻辑并没有发现问题,之后思考很久才发现,原因在于电梯并未正常结束,因为在电梯setEnd()时,很可能电梯类正要执行wait()操作,此时若setEnd()线程方法进行的比较快,提前进行了notifyAll(),之后电梯再wait(),那么电梯将不会再被唤醒,因在我的架构中,只有addPerRequest()setEnd()方法中有notifyAll(),这种bug触发的概率很低,本地也从未出现过(但鼠鼠很不幸被评测机抓到了两次),解决方法可以设置一个变量锁,必须在电梯wait()之后才能setEnd()

hw6

详情见第二次作业中的RESET请求,已经进行了分析。

hw7

依然会存在全部分给同一部电梯的情况(uu们的数据点都太刁钻了。。。),主要一个原因在于若某一部电梯已经到了新来请求的起始楼层,但此时电梯已经移动了,那该请求就需要等电梯返回才能接到,因此需要引入moveFlag才能避免这种情况。

心得体会

第二单元好难啊(灬ꈍ ꈍ灬),多线程不仅很难发现线程同步问题,还很难调试。我的调试方法就是采用打印相关变量,不过也算是有效。再有就是RTLE问题真的很难复现,强测中RTLE的点很有可能再交一遍就过了。。。同时我对于多线程的理解也只停留在简单的设置方法锁和唤醒,但变量锁显然更能提高性能,不过好在三次作业都能平稳地度过,自己在过程中也从小白到能编写基本的多线程程序,最后第七次作业强测也拿到了99分的成绩,也算是一个完美的收官(撒花鼓掌~) (醒醒,这只是第二单元

致谢

  • hyggge学长的博客,对我帮助很大
  • Yanna学姐的博客,对我调度策略的完善帮助很大
  • Thysrael 学长的博客,对各种调度策略的实现难度和性能进行了详细的介绍
  • 13
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

ribber160

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值