BUAA OO Unit2 多线程电梯调度 总结

钟昊翔-21231025 学生 2024-04-16 21:37:02

第五次作业

题目描述

我们要模拟的电梯系统是一个类似北京航空航天大学新主楼的电梯系统,楼座内有多部电梯,电梯可以在楼座内1-11层之间运行。系统从标准输入中读入乘客请求信息(起点层,终点楼层),请求调度器会根据此时电梯运行情况(电梯所在楼层,运行方向等)将乘客请求合理分配给某部电梯,然后被分配请求的电梯会经过上下行,开关门,乘客进入/离开电梯等动作将乘客从起点层运送到终点层。

可以采用任何电梯运行策略,即任意时刻,系统选择上下行动,是否在某层开关门都可以自定义,只要保证在电梯系统运行时间不超过题目要求时间上限的前提下将所有的乘客送至目的地即可。

输入格式

  • 每个乘客由指定的电梯从起点层接送到终点层。
  • 格式为:[时间戳]乘客ID-FROM-起点层-TO-终点层-BY-电梯ID

输出格式

  • 电梯到达某一位置:[时间戳]ARRIVE-所在层-电梯ID
  • 电梯开始开门:[时间戳]OPEN-所在层-电梯ID
  • 电梯完成关门:[时间戳]CLOSE-所在层-电梯ID
  • 乘客进入电梯:[时间戳]IN-乘客ID-所在层-电梯ID
  • 乘客离开电梯:[时间戳]OUT-乘客ID-所在层-电梯ID

同步块和锁

本次作业涉及到的共享对象只有WaitQueue,因此同步块主要在WaitQueue的添加和获取方法上,锁即waitQueue对象本身:

public synchronized boolean isEmpty();
public synchronized void addRequest(PersonRequest personRequest);
public synchronized boolean hasPassenger(int floor, int direction);
public synchronized Passenger getPassenger(int floor, int direction, boolean elevatorFree);
public synchronized void setEnd(boolean isEnd);
public synchronized boolean isEnd();

整体架构

img


img

调度方法

在本次作业中,我采用的方法是:假定每个乘客都是贪婪的,电梯是无脑的,具体来说可以简述为4步:

  1. 电梯空闲,选择最先到达的乘客,设定运行方向为该乘客的位置,并且朝着他的方向运行
  2. 电梯在运行途中,会在不超过载客量的前提下,携带同方向的乘客
  3. 当电梯当前运行方向的前方没人,并且电梯内也没人,电梯回到空闲状态
  4. 只要电梯不为空闲,就一直朝着运行方向前进

优点:好写

缺点:由于我是直接把InputThread的数据传入电梯的WaitQueue的,所以会导致一些问题

比如:如果在0时刻3层到达了一名乘客,电梯会向上运行,在运行到2楼的时候来了6个乘客,这样3层先来的乘客就会上不去电梯,如果电梯一直满员,那第一个到达的这个乘客就只能在输入结束后才会被处理。

第六次作业

题目描述

本次及以后作业不再指定电梯,需设计调度器分配电梯完成乘客请求。

输入格式

  • 每一行是一个乘客的到达信息。格式为:[时间戳]乘客ID-FROM-起点层-TO-终点层

新增:

RESET

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

输入格式

  • 电梯重置:[时间戳]RESET-Elevator-电梯ID-满载人数-移动一层的时间(单位s)

输出格式

  • 电梯接收到重置请求:[时间戳]RESET_ACCEPT-电梯ID-满载人数-移动一层的时间(单位s)(该消息由官方包自动输出,同学无需输出)
  • 电梯开始重置:[时间戳]RESET_BEGIN-电梯ID
  • 电梯重置完成:[时间戳]RESET_END-电梯ID

RECEIVE

为避免出现多部电梯接送同一乘客造成资源浪费的情况,引入RECEIVE约束。同时我们希望同学们将RECEIVE作为调度器的附加输出来说明自己的分配方案,边界情况见正确性说明

  • 电梯接收分配:[时间戳]RECEIVE-乘客ID-电梯ID

同学们需要在乘客进入电梯和电梯移动前输出RECEIVE来说明乘客请求分配情况。

  • 乘客只能在电梯外才可以有相关RECEIVE输出(请同学们注意OUT、IN和RECEIVE的输出顺序
  • 任何时刻任何一个乘客请求都至多会被分配给一部电梯,乘客只能进入RECEIVE输出规定的电梯。
  • 电梯内没有乘客且没有RECEIVE到某个乘客请求时的移动(输出ARRIVE)是不合法的移动。

同步块和锁

涉及到的共享对象,主要为两个请求队列(公共队列和电梯自己的等待队列)

在两个队列中,都设置了读写锁,和队列为空的等待Condition

private final ReadWriteLock lock = new ReentrantReadWriteLock(false);
private final Condition queueIsEmpty = lock.writeLock().newCondition();

在所有涉及到修改的方法,都使用了写锁,例如加入和获取,在加入方法会唤醒因为队列为空的等待:

public void addPassenger(Passenger passenger) {
    lock.writeLock().lock();
    publicQueue.add(passenger);
    queueIsEmpty.signalAll();
    lock.writeLock().unlock();
}
public Passenger getOnePassengerAndRemove() {
    lock.writeLock().lock();
    if (publicQueue.isEmpty()) {
        try {
            queueIsEmpty.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    if (publicQueue.isEmpty()) {
        try {
            return null;
        } finally {
            lock.writeLock().unlock();
        }
    }
    Passenger passenger = publicQueue.poll();
    try {
        return passenger;
    } finally {
        lock.writeLock().unlock();
    }
}

其他的查询方法都设置的读锁:

public boolean isEmpty() {
    lock.readLock().lock();
    try {
        return publicQueue.isEmpty();
    } finally {
        lock.readLock().unlock();
    }
}

电梯的代价计算并没有上锁,因为只涉及到楼层一个变量,如果拿到过时的数据也没关系,无非是多等会。

整体架构

img

img

调度方法

对于单个电梯,调度方法和上次一样,假定每个乘客都是贪婪的,电梯是无脑的,具体来说可以简述为4步:

  1. 电梯空闲,选择最先到达的乘客,设定运行方向为该乘客的位置,并且朝着他的方向运行
  2. 电梯在运行途中,会在不超过载客量的前提下,携带同方向的乘客
  3. 当电梯当前运行方向的前方没人,并且电梯内也没人,电梯回到空闲状态
  4. 只要电梯不为空闲,就一直朝着运行方向前进

对于调度器,每次从公共队列拿出最先到达(时间戳最早)的乘客,并且默认每个乘客都是短视的,总体来说可以分为两步:

  1. 寻找到达自己等待楼层,并且运送到到目的地楼层用时最短的电梯(不考虑中途开关门上人,也就是说他默认这步电梯只会为他服务),选定之后直接进入电梯的队列,一直等待。
  2. 如果电梯到达乘客等待楼层,并且人满为患,则遣返所有没有在本层成功上电梯的人至公共队列。

优点:好写

缺点:好像性能还挺不错?除非构造所有电梯同步满载运行(和前面说的一样)的数据,不然好像没什么太大问题。

第七次作业

题目描述

新增:双轿厢电梯运行

双轿厢电梯是指在同一电梯井道内同时拥有两个独立的电梯轿厢,而电梯系统默认的普通电梯是指在一个电梯井道内只有一个轿厢。为了保证两个轿厢不相互碰撞,将楼层分为上区、下区、换乘楼层,其中上区为换乘楼层以上的所有楼层,下区为换乘楼层以下的楼层,均不包含换乘楼层。在整个运行过程中,要求轿厢 A 只能在下区和换乘楼层运行,轿厢 B 只能在上区和换乘楼层运行,同一井道内的两轿厢不能同时位于换乘楼层

输入格式:

  • [时间戳]RESET-DCElevator-电梯ID-换乘楼层-每个轿厢的满载人数-每个轿厢移动一层的时间(单位s)

同步块和锁

和上一次几乎一样,新加入的方法加锁原则也一样,如果涉及数据修改就加写锁,反之就加读锁。

整体架构

img

img

调度方法

对于单个电梯,调度方法和第一次次一样,假定每个乘客都是贪婪的,电梯是无脑的,具体来说可以简述为4步:

  1. 电梯空闲,选择最先到达的乘客,设定运行方向为该乘客的位置,并且朝着他的方向运行
  2. 电梯在运行途中,会在不超过载客量的前提下,携带同方向的乘客
  3. 当电梯当前运行方向的前方没人,并且电梯内也没人,电梯回到空闲状态
  4. 只要电梯不为空闲,就一直朝着运行方向前进

对于调度器,每次从公共队列拿出最先到达(时间戳最早)的乘客,并且默认每个乘客都是短视的,总体来说可以分为两步:

  1. 寻找到达自己等待楼层,并且运送到到目的地楼层用时最短的电梯(不考虑中途开关门上人,也就是说他默认这步电梯只会为他服务),选定之后直接进入电梯的队列,一直等待。

    对于双轿厢电梯,在时间的计算上分成上下两部分,简单相加即可,不需要太精确,毕竟我们认为乘客都是短视的嘛>_<

  2. 如果电梯到达乘客等待楼层,并且人满为患,则遣返所有没有在本层成功上电梯的人至公共队列。

优点:好写

缺点:好像性能还挺不错?甚至强测性能分数比上一次还要高?我也不知道有什么缺点,反正麻烦的事情都交给电脑做,我的脑子得到了很大的放松。最后强测得分99.97,我觉得投入产出比还是很高的。

如何处理修改为双轿厢的请求?

我的做法是收到这个请求的电梯调度器执行3个动作:

  1. 将自己和控制电梯设置为STOP,这样调度器将不会分配乘客
  2. 创建两个新的调度器线程,他们会对应自己的电梯,之后将其加入电梯列表
  3. 清空原有电梯内和等待队列内所有人(遣返至公共队列)

这样就约等于创建两个新的独立电梯,并且删除旧的电梯,除了移动进共享楼层,其他的移动策略和普通没什么区别。

如何保证不碰撞

我采用了"共享楼层锁"的方法

  • 针对电梯的move()方法,在即将进入共享楼层的时候尝试取得锁,如果不成功就会wait,离开共享楼层的时候释放锁,并且在尝试进入之前会先向另一部电梯发出移开信号。

    public void move() {
        ...
        if (floor + direction == transferFloor) {
            Move move = new Move(id + "-" + transferFloor, -transferDirection);
            friendPool.addRequest(move); // 向同轿厢发出Move请求
            sharedFloorLock.enterSharedFloor();
        }
        ...
        if (floor == transferFloor) {
            sharedFloorLock.exitSharedFloor();
        }
        ...
        floor += direction;
    }
    
    public class SharedFloorLock {
        ...
        private boolean free = true;
        private final ReadWriteLock lock = new ReentrantReadWriteLock();
        private final Condition waitForFree = lock.writeLock().newCondition();
        ...
        public void enterSharedFloor() {
            lock.writeLock().lock();
            if (!free) {
                try {
                    waitForFree.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            free = false;
            lock.writeLock().unlock();
        }
    
        public void exitSharedFloor() {
            lock.writeLock().lock();
            free = true;
            waitForFree.signalAll();
            lock.writeLock().unlock();
        }
    }
    

三次作业中稳定和变化的地方

稳定的地方应该就是电梯的调度和移动,三次作业几乎没有变化

调度方法也是几乎不变的(对于我的方法),在调度器里面,只有简单的根据代价选择最小的电梯,因此每次只需要改变代价计算函数即可,调度器本身没什么变化

public void run() {
    while (true) {
        ...
        Passenger passenger = publicPool.getPassengerAndRemove(true);
        if (passenger == null) {
            continue;
        }
        ...
        findMinCostElevatorAndAdd(passenger);
    }
}

变化的地方首先就是代价计算函数了,这部分其实算的不太严谨也没事,因为每个乘客总有一个能上的电梯,因此对正确性不会有什么影响,无非就是慢点罢了。

其他的地方好像都没什么太大变化?

心得体会

多线程真的好难>...<,虽然三次作业都结束了,但是感觉自己对架构和线程间协作的了解还是不太深刻。各种同步块、锁容易写的很混乱,造成死锁或者数据冲突,不过还是学到了很多知识。

Debug方法

由于不会高级的debug方法,因此主要是靠print来看电梯运行时候的信息。

为了方便开关,写了个类来控制输出,并且还有点颜色,挺好看的。

主要是把电梯涉及到的所有状态变量都打印出来,还是在debug的过程中起到了不少帮助。

public class DebugLog {
    private static final boolean debug = true;
    private static final HashMap<String, Boolean> bannedClass = new HashMap<String, Boolean>() {{
            put("", true);
        }};
    private static final HashMap<String, Boolean> bannedId = new HashMap<String, Boolean>() {{
            put("0", false);
            put("1", false);
            put("2", false);
            put("3", false);
            put("4", false);
            put("5", false);
            put("6", false);
        }};
    private static final HashMap<String, Boolean> bannedInfo = new HashMap<String, Boolean>() {{
            put("", true);
        }};
    
    // Debuggable接口作用就是返回类名,这样在调用的时候只需要DebugLog.println(this, "..."),比较省事
    public static void println(Debuggable obj, String info) {
        String[] className = String.valueOf(obj.getClass()).split(" ");
        println(className[1], obj.getDebugId(), info);
    }

    private static void println(String classname, String id, String info) {
        if (!debug) {
            return;
        }
        if (bannedClass.containsKey(classname)) {
            if (bannedClass.get(classname)) {
                return;
            }
        }
        if (bannedId.containsKey(id)) {
            if (bannedId.get(id)) {
                return;
            }
        }
        if (bannedInfo.containsKey(info)) {
            if (bannedInfo.get(info)) {
                return;
            }
        }
        switch (classname) {
            case "InputThread": {
                System.out.printf("\033[1;32m(%s-%s)\033[0m: %s%n", classname, id, info);
                break;
            }
            case "HumanController": {
                System.out.printf("\033[1;36m(%s-%s)\033[0m: %s%n", classname, id, info);
                break;
            }
            case "RequestPool": {
                System.out.printf("\033[1;33m(%s-%s)\033[0m: %s%n", classname, id, info);
                break;
            }
            case "Elevator": {
                System.out.printf("\033[1;34m(%s-%s)\033[0m: %s%n", classname, id, info);
                break;
            }
            case "ElevatorController": {
                System.out.printf("\033[1;35m(%s-%s)\033[0m: %s%n", classname, id, info);
                break;
            }
            default: {
                System.out.printf("\033[1m(%s-%s)\033[0m: %s%n", classname, id, info);
            }
        }
    }
}
...全文
360 回复 打赏 收藏 转发到动态 举报
写回复
用AI写文章
回复
切换为时间正序
请发表友善的回复…
发表回复

301

社区成员

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

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