第二单元:电梯调度

前言

OO第二单元的主题是“多线程”,我们需要学习多线程的编程思想,运用多个线程解决问题。其中,要解决多个线程之间的共享数据涉及到的线程交互,线程安全问题。面对着很多时候无法复现的bug和刚刚写完的代码报出的ConcurrentModificationException这类的错误,可以说“电梯月”充满了艰难。

除了保证正确性,我们还需要考虑调度策略来提高自己时间和电量上的性能。

第五次作业

本次作业的任务主要是模拟北航新主楼的电梯系统,对定时投放的乘客请求进行处理,将乘客从请求楼层运送到目标楼层。因为在输入时已经指出乘客需要乘坐的电梯ID,所以说本次作业也无需设计调度器,整体来说实现比较简单。

代码UML类图

本次作业代码UML类图如下所示

代码架构分析

第一次作业中每部电梯只需要管理好自己的运行策略即可,较为简单。除了主线程,我一共开启了1个输入线程、和6个电梯线程。

生产者-消费者模型

我们不难判断出本次作业中谁是生产者,谁是消费者。

  • 输入线程(InputHandler):从终端中获取乘客信息,所以输入线程是生产者。

  • 电梯线程(Elevator): 对于用户的请求进行处理,将用户送到指定位置,电梯线程为消费者。

生产者与消费者之间的共享数据在这里也就是requestable,乘客需求容器,相当于输入线程向这里投放需求,电梯线程消耗需求。

我将乘客封装成了一个Passenger类,将乘客信息封装在这里。

//Passenger.java
public Passenger(int id, int fromFloor, int toFloor, int byElev) {
        this.id = id;
        this.fromFloor = fromFloor;
        this.toFloor = toFloor;
        this.byElev = byElev;
}

Requestable类中用一个HashMap容器来存储乘客需求,我的设计中使用了乘客发出请求的楼层作为key,同一层的乘客需求集合(Arraylist)作为value,这样就可以在电梯运行接乘客的时候方便电梯定位到乘客。为每个电梯都实例化一个requestable对象,来记录电梯接收到的请求。

同步块的设置和锁的选择

这次作业中的共享数据只有Requestable一个类,也即输入线程和电梯线程可能会同时读写Requestable类实例化的对象,所以我的设计中对Requestable类中涉及到对其成员变量读写的所有方法全部加锁。保证同一时间只有一个线程读写访问乘客请求容器。

LOOK策略

在了解了很多电梯捎带策略,并且参考了许多学长的博客之后,我最终选择了LOOK电梯调度策略。平均性能占优并且比较稳定。LOOK策略也被我沿用到了第七次作业。

策略类的设置

本次作业中我比较满意的是我电梯类与策略类实现了分离,实现了功能上的剥离,让我的电梯看起来有些“无脑”。当电梯到达一个楼层时,电梯就会询问自己的strategy下一步该做什么。我用一个枚举类Advice封装了strategy可以给电梯的几种建议。

  • MOVE:电梯沿当前方向移动一个楼层。

  • REVERSE:电梯调转移动方向。

  • OPEN:电梯开关门。

  • WAIT:电梯等待需求。

  • END:结束电梯线程。

这样提高了代码的可读性。

//Strategy.java
public Advice getAdvice(int curFloor, int curNum, boolean direction,
                            HashMap<Integer, HashSet<Passenger>> boardMap) {
        //判断现在是否能够上下电梯
        if (canOpenForOut(curFloor, boardMap) || canOpenForIn(curFloor, curNum, direction)) {
            return Advice.OPEN;
        }
        //如果电梯里面还有人
        if (curNum != 0) {
            return Advice.MOVE;
        }
        //如果电梯里面没有人
        else {
            //如果请求队列中没有人
            if (requestTable.isEmpty()) {
                if (requestTable.isEnd()) {
                    return Advice.END;
                } else {
                    return Advice.WAIT;
                }
            }
            //如果请求队列中有人
            if (hasReqInOriginDirect(curFloor, direction)) {
                return Advice.MOVE; //如果有请求在前方,并且方向一致
            } else {
                return Advice.REVERSE;
            }
        }
    }

代码复杂度分析

其中Strategy类中的getAdvice和hasReqInOriginDirection方法复杂度较高,向电梯提供建议(需要根据电梯当前状态和请求队列)和判断目前方向上(需要请求队列)是否有请求难免用到比较多的分支和循环,所以复杂度较高。

第六次作业

本次作业的任务是乘客在发出请求的时候不再指定电梯,而是需要我们调度分配乘客的请求给一个指定的电梯。另外,电梯新加了reset操作,reset的时候电梯处于静默状态,即不能移动、开关门、接受请求等。另外,课程组为了限制省事并且性能好的自由竞争,要求我们在电梯接受到请求时输出RECEIVE。

代码UML类图

本次作业代码UML类图如下所示

代码架构分析

本次作业在上一次作业的基础上进行迭代,我们亟需解决一下问题:

  • 如何实现电梯的reset操作

  • 如何合理分配请求,尽量满足时间和电量上的性能需求

  • 电梯reset的时候不能被分配到请求,那么所有电梯如果同时reset那请求应该分配到哪里。

总的来说,本次迭代我们主要新增的东西有两个,一个是调度器,另一个就是电梯reset操作的实现。

关于调度器

不难发现,我们如果想让性能高,我们就需要让电梯尽快的把所有乘客都送完,同时电梯可以尽量的少移动、少开关门。

由于我们没有办法预知下一时刻会不会有新的需求,新的需求有多少,是什么。所以我们只能根据所有电梯当前的情况来调度需求。这里就要介绍一下影子电梯的方法,这也是很多同学都在使用的方法,在局部最优的道路上可以说性能很赞。

影子电梯的方法是,对于每一个新来的请求,我们通过深克隆电梯(elevator)和电梯的请求队列(requestable),之后模拟运行(这里并不需要新开线程,我们只需要对一个totaltime进行累加)算出现在如果将新的请求加入该电梯的请求队列,电梯运行到结束(end)需要的总时间totaltime。这个过程中我们还可以算出电梯移动和开关门所用的总电量。根据课程组计算性能的方法对时间和电量进行加权计算就可以到得到对每个电梯的“评价”,我们选择一个最优的电梯进行分配即可。

这次作业中我们无法将需求直接加到某个电梯的请求队列,所以在我的设计中新增了一个RequestPool类,即总需求池,一个大盘子。

新增Schedule调度线程,Schedule线程通过掌握电梯当前状态信息和电梯侯乘表来调度新的需求。

关于一个hack了很多人的数据

这次作业中因为有地方没处理好代码中出现了死锁,进了C房。。。也是被这个bug伤到了,所有电梯进行reset的同时输入一大堆请求。在我原来的设计中,电梯只要在reset状态就不能被分配请求,所以我原来的schedule线程就卡在了这里,出现了超时的问题。

在bug修复的过程中,我改变了设计,为每个电梯新增了一个receiveQuene。电梯reset的时候不是继续处于不能够接受乘客需求的状态,而是schedule可以将请求分配给reset的电梯,只不过电梯没有接触reset状态的的时候不输出 [时间戳]RECEIVE-乘客ID-电梯ID。这样,在电梯输出 [时间戳]RESET_END-电梯ID 之后我们先输出Receive,之后将电梯的receiveQuene清空,加入到正常的侯乘表(此时会唤醒电梯线程)。这样也不会出现电梯在输出RECEIVE之前移动的情况,问题得到解决。

同步块和锁的设置

这次作业的锁的设置比上次作业复杂很多。RequestTable仍然是共享对象,需要被Schedule线程和Elevator线程访问;RequestPool需要被输入线程InputHandler和Schedule线程访问,所以RequestPool的所有访问属性的我也加了锁。

进行新需求分配的时候,Elevator类也要被Schedule获取,Schedule类需要知道当前电梯是否处于reset状态,所以Schedule类中我对elevator对象也进行了加锁。

代码复杂度分析

schedule.run方法复杂度较高,主要是在判断调度器线程什么时候结束判断条件比较复杂。

第七次作业

本次作业的迭代内容是电梯可以进行第二种reset,不同于上一次作业的reset单纯修改电梯的一些参数,第二种reset可以让电梯分裂成双轿厢电梯,即上下BA两部电梯,两部电梯只能够在换乘楼层和各自的上下半区进行移动。

代码UML类图

本次作业代码UML类图如下所示

代码架构分析

这次迭代我们亟待解决一下三个问题:

  • 如何将一部电梯重置为双轿厢电梯

  • 新增双轿厢电梯后,如何调度乘客请求

  • 如何保证双轿厢电梯之间不相撞

本次作业,我新增了DcResetRequest类继承MyResetRequest类来封装第二类电梯重置请求。新增Flag类用于防止双轿厢电梯相撞。

防止相撞

我的设计中,同一楼座的AB电梯共享一个枚举类State实例化的state对象,我们可以通过对state上锁来实现AB电梯不会相撞。比如当电梯A“到达”换乘楼层后,A电梯需要看state是否处于Occupied状态,如果是,则flag进入等待状态,等待B电梯离开换乘楼层即输出Arrive到达另一楼层后唤醒A电梯的flag。A电梯flag唤醒之后,才能够输出A电梯的Arrive换乘层。

在这个设计中,我们需要实现在A或B电梯为空且请求队列也为空的时候电梯如果在换乘楼层则需要自动移动一层,腾出换乘层。

//Flag.java
public class Flag {
    private State state;
​
    public Flag() {
        this.state = State.UNOCCUPIED;
    }
​
    public synchronized void setOccupied() {
        waitRelease();
        state = State.OCCUPIED;
        notifyAll();
    }
​
    public synchronized void setRelease() {
        this.state = State.UNOCCUPIED;
        notifyAll();
    }
​
    public synchronized void waitRelease() {
        notifyAll();
        while (state == State.OCCUPIED) {
            try {
                notifyAll();
                wait();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}
//elevator.java
if (curFloor == transferFloor) {
                occupied.setOccupied();
            }
            TimableOutput.println(String.format("ARRIVE-%d-%d-%s",
                    curFloor, getRealId(), statusToString()));
            if ((direction && curFloor - 1 == transferFloor) ||
                    (!direction && curFloor + 1 == transferFloor)) {
                occupied.setRelease();
            }

同步块和锁的设置

本次作业的所得设置基本与上一次作业相同,只新增了Flag类中的锁。

Debug方法

第二单元的debug真的是“好开心啊”,有的bug要输入十次可能才会复现出来一次,所以说debug首先需要的是耐心。

对于死锁类bug,第六次和第七次作业中有很多地方出现了死锁,线程无法唤醒的情况。这个时候我使用输出方法,在wait的前后分别加上不同的输出,确认哪个线程没有被唤醒,之后对唤醒的条件分别进行输出,找到没有唤醒的原因,这样就可以比较快速地定位到bug。

对于其他的逻辑类bug,我们可以根据评测机的报错信息或者简化输入数据从而将输出减短,使输出更容易观察来找到问题所在。

心得体会

  • 加锁要慎重,不能盲目加锁

  • 善用wait-notify方法,防止CPU轮询

  • 迭代时要注意好有哪些参数需要被重新设置,防止出现我的第七次作业因为新电梯没重新设置maxPassenger参数没进互测。

  • 良好的架构真的会让迭代和debug事半功倍啊!

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值