BUAA_OO_2025 第二单元总结

王晨希-23373502 2025-04-20 23:35:30

BUAA_OO_2025 第二单元总结

引言

本单元要求我们实现一个多个电梯的协作系统,主要目标是:

  1. 掌握线程间的交互逻辑与协同设计层次架构。
  2. 理解调度器的作用与实现的方法。

三次作业对我们的要求分别是:

  1. 第一次作业:每个乘客只可以被分配给固定的电梯,每个电梯对自己要处理的队列进行决策如何追求最高的效率

  2. 第二次作业:

  • 不再固定每个乘客乘坐的电梯,同时加上Schedule请求,相当于一次中断处理。
  • 增加Receive约束,必须先Receive再进入
  1. 第三次作业:增加双轿厢电梯的情况,让两者在同一电梯井里进行决策

同步块和锁的设计

这一部分主要是考察的我们在程序中对synchronized,wait,notifyall的使用:

  • 我们要在合适的地方对一个对象加上锁,并合理的释放锁,防止它长时间占有。这里主要需要运用好synchronized,对方法、对象等加锁。

  • 当电梯没有请求需要处理的时候,要让他释放锁,并且进入wait状态,直到其他线程对队列进行更改时,用notifyall唤醒电梯并重新进行决策。

具体实现如下:

  1. synchronized的使用示例:

    public class RequestList {
        private final ArrayList<Person> requestQueue = new ArrayList<>();
        private boolean isEnd = false;
        private int remain = 0;
        public synchronized void offer(Person request) {
            int i;
            for (i = 0; i < requestQueue.size(); i++) {
                if (request.getPriority() > requestQueue.get(i).getPriority()) {
                    break;
                }
            }
            requestQueue.add(i, request);
            notifyAll();
        }
        public synchronized Person removeFirst() {
            if (!isEmpty()) {
                return requestQueue.remove(0);
            } else {
                return null;
            }
        }
        ......
    }
    

    ​ 在几个队列类中我都将它们中所有的方法都进行了synchronized包裹。因为每一个队列都会在不同的现场中被读或写,而要保证每一次访问它时,不会造成冲突,那么就要让它的每一个方法都带锁。

    ​ 不过其实可以让它的方法分为两类:读操作和写操作,合理利用读写锁可以更高效率的对这个对象进行访问,不过由于代码运行的时间相比于线程处理的时间可以忽略不计,使用没有实现读写锁,只用了简单的synchronized

  2. waitnotifyall的协作:

        public synchronized void waitRequest() {
            if (isEmpty() && !isEnd()) {
                try {
                    wait();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        }
    

    ​ 我的代码中的wait只会在电梯发现队列为空,但还不能终止的时候,调用waitRequest时才会使用。可能一些其他同学会在读操作中使用,但我觉得它并没有必要等待,可以直接返回null

    ​ 那已知会使用wait的条件,我们就只需要在这些条件有可能被更改时再进行notifyall即可。那么我只有在增加队列中请求个数offer和标记输入结束setEnd时才需要唤醒(在程序中可能还有一些特例),其余的唤醒只会让电梯重新获取策略,造成不必要的资源浪费。

调度器设计与调度策略

乘客分派

在这里列举一些分派策略的实现思路:

1.随机分配,直接id = r.nextInt(6)+1即可。

2.均匀分配,对6取余+1,跳过不能分配的。

3.评价体系,按照性能分指标,为当前电梯状态的每个参数赋值一个权重,得到一个综合分,每次都分给分数最高的那个。

4.影子电梯:每当新增了一个乘客请求,在真正分配之前我们分别模拟一下将其分给6个电梯哪个效果最好。那么我们就要给每个电梯当前状态建立一个“影子”,让影子去提前跑一个结果,只要用时间变量time的增加取代,sleep部分即可,这样就可以得到用时最短的那个方案。

我的思路

​ 我参考了现实中我们等电梯的等待方式。我们会一群人在几个电梯中间,不去只关注一个电梯,而是哪个电梯到了,我们尝试往里面“挤”。那么分派的标准就不是来一个人就要让他必须去等那一个电梯,而是每当一个电梯到达了一个楼层,在该楼层中选择人上电梯(也就是我们自己挤上去的过程)。那么我就完全省去了分派这一过程。

  • 具体实现:和每一个电梯占有自己的子队列一样处理,只是每个电梯都共享了同一个子队列。

  • 性能分析:在我的想法中,而分派过程是考虑了一个暂时最优的策略,将请求直接分走。而每一个电梯对自己的子队列都有一个调度策略,这个策略是与时俱进的,可以不断更新队列。那么省去分派过程就是只采用电梯调度策略,一定性能优于先分派再调度。

  • 不过这个方法需要经常对队列加锁,更有可能出错,读写操作也需要更多时间。不过我认为这个时间并不会大于性能带来的好处。

电梯运行的调度策略

主要是在Look算法的基础上,增加了优先级的策略ALS

  • 主请求选择规则:
    1. 如果电梯中没有乘客,选择最高优先级的请求为主请求;如果多个请求具有相同的最高优先级,选择到达时间最早的请求。
    2. 如果电梯中有乘客,选择最高优先级的乘客为主请求;如果多个乘客具有相同的最高优先级,选择到达时间最早的请求。(不过这个因为Look已经保证方向,并没有影响)
  • 被捎带请求选择规则:
    1. 电梯的主请求存在
    2. 该请求投喂的时刻小于等于电梯到达该请求出发楼层关门的截止时间
    3. 电梯的运行方向和该请求的目标方向一致

策略类实现如下:

    if (schedule != null) {
        return Advice.SCHEDULE;
    }
    if (receivePerson != null) {
        if (curFloor != receivePerson.getFromFloor()) {
            return Advice.MOVE;
        } else {
            return Advice.OPEN;
        }
    } 
    if (needOpenForOut(curFloor, destMap) ||
        canOpenForIn(curFloor, curNum, direction, requests)) {
        return Advice.OPEN;
    }
    //如果电梯里有人
    if (curNum != 0) {
        return Advice.MOVE;
    }
    //如果电梯里没有人且无法进人
    else {
        //如果请求队列中没有人
        if (requests.isEmpty()) {
            if (requests.isEnd() && scheRequests.isEnd()) {
                return Advice.OVER; //如果输入结束,电梯线程结束
            } else {
                return Advice.WAIT; //如果输入未结束,电梯线程等待
            }
        }
        //如果请求队列有人
        return Advice.RECEIVE;
    }
  • SCHE请求优先级最高。

  • 由于我的所有电梯共享一个队列,所以不存在receive到的子队列,那么我每次只在电梯每人时找一个优先级最高的receivePerson,先把他接到再进行其他操作。所以RECEIVE优先级很高。

  • 每当电梯到达一个楼层需要确定是否开门。即可能有人到了,也有可能等待队列中该楼层有人要往相同方向走。(当没人时可以找不同方向的)

  • 如果不用开门,且电梯有人,就正常运行到direction上的下一个楼层。

  • 如果没人且不需要开门,那么如果队列为空,就决定继续等待还是终止线程。如果不为空,则去找一个receivePerson接他。

架构模式

hw5架构

img

输入线程InputThread将请求输入给RequestList,再通过DispatchThread分派给每个电梯的子队列RequestTable。电梯线程中封装一个Strategy策略类,实现电梯类和策略类分离,每次将电梯的状态和RequestTable传入进行决策返回建议Advice。电梯线程中通过得到的Advice来运行。

hw6架构

img

相比于上一次直接删去了子队列类。其余就是增加了ScheRequestLlist的输入和分派,以及在电梯线程中增加了schedule的完整方法。其余分派和receive实现见上文。

hw7架构

img

我将UPDATE请求同样放入ScheRequestList中进行分派,省去了一定的代码量。新增了UpdateHandler用于储存UPTADE之后的信息。我没有新开双轿厢电梯类,而是把电梯和UpdateHandler同时传入新的策略类UpdateStrategy,即可得到双轿厢电梯的信息和另一个电梯的状态。UPDATE后就舍弃Strategy,只用UpdateStrategy进行决策,这样可以保证原来的正常电梯运行不受影响,Strategy也不用更改。

UML协作图

img

稳定和易变的内容

  • 稳定的内容:

    1. 输入线程
    2. SCHEUPDATE这类特殊请求的分派和处理。
    3. 请求本身的属性和方法
  • 易变的内容:

    1. 电梯线程的不同操作

    2. 队列的读写操作

    3. 新情况下策略类Strategy的决策方式

如何实现双轿厢的同步开始改造和运行时两个轿厢不碰撞的

同步开始改造

    //电梯线程中识别到要进行改造
    synchronized (updateHandler) {
        updateHandler.increaseReady();
        if (!updateHandler.isReady()) {
            try {
                updateHandler.wait();
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }
    }
    //UpdateHandler工具类,让两者同一储存
    public synchronized void increaseReady() {
        ready++;
        if (isReady()) {
            TimableOutput.println("UPDATE-BEGIN-" + getElevatorAId()
                + "-" + getElevatorBId());
        }
        notifyAll();
    }

其他如让电梯里乘客下电梯,等待一秒省略。

当结束时,也与上述相似,等到两个都完成时,统一输出。

运行时两个轿厢不碰撞

  • 每一步要进行移动之前都判断是否移动会碰撞,如果前方有电梯占用,则返回WAITTOMOVE,即等待一段时间(sleep)再来重新查询:

    if (curNum != 0) {
          if ((direction && curFloor == transferFloor - 1 &&
                anotherElevator.getCurFloor() == transferFloor) ||
                (!direction && curFloor == transferFloor + 1 &&
                anotherElevator.getCurFloor() == transferFloor)) {
                return Advice.WAITTOMOVE;
          }
          return Advice.MOVE;
    }
    
  • 即便是返回的是MOVE也要特判一下,因为有可能两个电梯都往transferFloor移动,还没移动到,另一个就得到了advice。因此,需要先等待0.2秒,然后再判断此时另一个电梯是否在transferFloor,如果不是才可以进行真正的移动。

出现过的bug以及面对多线程程序的debug方法

出现过的bug

  • 前两次作业都没有出现bug

  • 第三次作业由于写的比较匆忙,留下了一些隐患:

    1. 在update要把人赶出去的时候,clear写错了位置。

    2. 同步开始改造的begin输出有问题,没有完全同步

    3. update后wait的实现有问题,导致轮询

  • debug方法

    1. 通过评测机找到错误位置后,根据输出看为什么会发生这种错误,可能在什么地方的判断有问题

    2. 我的主要问题主要还是轮询,所以我会在每个可能导致轮询的地方加上输出,看什么时候会突然输出很多行,然后看是什么地方的wait或加锁错误。

心得体会

多线程编程中最容易出的问题就是线程安全问题。我个人采用的主要是synchornized对于共享对象的方法加锁与部分引用处synchronized(obj)加锁。在刚开始,我主要是根据实验课的代码进行修改,没有完全理解自己写的代码,后来在改代码的过程中才慢慢理解。通过本单元学习,我认为应该注意以下的线程安全问题:

线程安全

  1. 分清哪些对象是共享对象(即会被多个线程访问的对象)。然后根据需要判断每一处的访问是否需要加锁。
  2. 对于每一个wait判断什么时候应该唤醒它,在其他方法处是否加了正确的notifyAll,以及在while循环中使用wait等待,不要滥用notifyall,避免意外唤醒
  3. 避免将容器作为参数或返回值传递,避免ConcurrentModificationException问题
  4. 线程的结束条件,不能提前结束,也不能在该结束的时候wait

层次化设计

  • 本单元的顶层是生产者和消费者模型,InputHandler输入,DispatchThread调度派发,ElevatorThread完成消费者功能与输出。同时为每个电梯封装实现自己的策略类,不要全都一股脑在电梯线程中实现。
...全文
13 回复 打赏 收藏 转发到动态 举报
写回复
用AI写文章
回复
切换为时间正序
请发表友善的回复…
发表回复

269

社区成员

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

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