BUAA_OO 第二单元:电梯调度 总结

前言

春风若有怜花意,可否许我再U2

第二单元电梯调度我认为有两条主线需要注意,一个是线程安全,另一个是设计好的调度算法来提升性能。本人大部分精力是放在了线程安全上,故从本文中能得到的性能提升方面的分享并不多(而且大多是我从别人那听来的,自己并未实现,只有思路简述),而我更多的分享是站在普通人的视角,在保证正确性、安全性的前提下来介绍OO最具可玩性的电梯单元

本人自认为第二单元的完成度很低,知道很多有趣的想法但无精力去实现,自认为是一次很失败了的旅途,但总觉得要留下一点自己来过的痕迹。故本文不止含有自己代码的分析,也包含从其他同学那里学习来的好的设计方法。本文分篇章进行:三次迭代—宏观分析篇、多线程—线程安全篇、调度类—性能提升篇。同时本人提炼了一些关键词,供使用jal指令跳转(其实自己ctrl+F找一下啦,有些部分只是做了简要陈述):

  • Look策略
  • 两种类型RESET
  • Buffer缓冲方法
  • 计数器确定线程结束条件
  • 换乘楼层互斥
  • 轮询
  • 死锁
  • 量子电梯
  • 弹射起步
  • 影子电梯
  • 评价调度
  • 自由竞争
  • 摩天轮调度

三次迭代—宏观分析篇

第一次迭代

任务简析

这次任务相对简单,甚至未引入调度策略,我猜测课程组的意图是为了让我们先建立起多线程基本架构,再此基础上再做迭代。故主要需要实现:

  • 全局分发请求线程(InputHandle)
  • 电梯线程(Elevator)
  • 运行策略(Strategy)
UML类图

在这里插入图片描述

  • 结合任务分析,这次迭代含有两个线程:输入线程、电梯线程。依托 RequsetQueue —— 等待队列这一共享对象,实现生产—消费者模式。电梯运行策列依托于 Straregy 这一依赖注入类,根据 Strategy 返回的电梯下一个运行状态指导电梯运行(状态模式
具体实现
电梯运行策略类 LookStrategy

采用状态模型,输入电梯当前参数状态(电梯内的乘客表,电梯候乘表,电梯当前楼层等等),经过分析返回电梯下一时刻运行状态(移动、开门、转向等等),因此电梯类 Elevator 中线程运行 run 方法也就是不断进行状态间的转移。本人将电梯分成以下几个状态:OVEROPENREVERSEMOVEWAITING,对应实现代码如下:

//Elevator.java
public void run() {
        while (true) {
            ElevatorState nextElevatorState = strategy.getNextState(this);
            if (nextElevatorState == OVER) {
                break;
            } else if (nextElevatorState == MOVE) {
                move();
            } else if (nextElevatorState == REVERSE) {
                direction = !direction;
            } else if (nextElevatorState == WAITING) {
                synchronized (requestQueue) {
                    try {
                        requestQueue.wait();               //wait等待队列中addPassenger方法将其唤醒
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
            } else if (nextElevatorState == OPEN) {
                openAndClose();
            }
        }
    }

//LookStrategy.java
public ElevatorState getNextState(Elevator elevator) {
        int currentFloor = elevator.getCurrentFloor();
        int passengerNum = elevator.getPassengerNum();
        ...//还需要获取其他的电梯信息
        if (canOpenElevatorForOut || canOpenElevatorForIn) {    //此处省略了方法传递的参数
            return OPEN;
        }
        if (passengerNum != 0) {
            return MOVE;
        } else {
            if (requestQueue.isEmpty()) {
                if (requestQueue.isEnd()) {
                    return OVER;
                } else {
                    return WAITING;
                }
            } else {
                if (hasReqInDirection) {             //等待队列中有和电梯运行同方向的请求
                    return ElevatorState.MOVE;
                } else {
                    return ElevatorState.REVERSE;    
                }
            }
        }
    }

上面的Look策略是我结合学长博客且稍夹带一点自己的改动所写,其中的捎带原则是只捎带与电梯运行方向同向的乘客。

不过只是这样的话就遇到了一个问题:当电梯在最高层开门后放出全部乘客且又进入乘客,此时根据 LookStrategy 中的逻辑,下一个状态应该是MOVE (因为passengerNum != 0 ,但此时是最高层啊),这是因为缺少一次转向状态 REVERSE 的状态转移 ,故我在 OPEN 状态中增加这一次状态转移的判断:

//Elevator.java
private void openAndClose() {
    open();
    outPassenger();
    ElevatorState state = strategy.getNextState(this);
    if (state == ElevatorState.REVERSE) {
    		direction = !direction;
    }
    inPassenger();
    close();
}

这个小设计来自于我心目中六系第一人zyt,当然也可以通过修改上面的LookStrateggetNextState 方法的逻辑来规避这个问题,不过很多时候这种小聪明、小设计很能打动我。

生产—消费者模式的托盘:RequestQueue

关于生产—消费者模式这里只做简单描述,后续会在多线程—线程安全篇细讲。

//RequestQueue.java
public class RequestQueue {
    private final List<Passenger> passengers;
    private final HashMap<Integer, ArrayList<Passenger>> requestQueueByFromFloor;
    private final HashMap<Integer, ArrayList<Passenger>> requestQueueByToFloor;

    private boolean isEnd;

    public synchronized void addPassenger(Passenger passenger, int elevatorId) {
        passengers.add(passenger);
        requestQueueByFromFloor.get(passenger.getFrom()).add(passenger);
        requestQueueByToFloor.get(passenger.getTo()).add(passenger);
        notifyAll();
    }

    public synchronized void delPassenger(Passenger passenger) {
        passengers.remove(passenger);
        requestQueueByFromFloor.get(passenger.getFrom()).remove(passenger);
        requestQueueByToFloor.get(passenger.getTo()).remove(passenger);
    }

    public synchronized void setEnd(boolean isEnd) {
        this.isEnd = isEnd;
        notifyAll();
    }

    public synchronized boolean isEnd() {
        return isEnd;
    }

这里只列举了部分属性和方法,其中对于乘客本人不止用 List 顺序存储,还用两个HashMap 来维护,目的是通过乘客的原始楼层From 或目的楼层To 可以快速索引到,是一种典型的以空间换时间的方法。

方法上,由于是共享对象,全局分发请求线程可以调用add方法放入乘客,电梯线程可以调用del方法接走乘客,故方法需要用互斥锁synchroonized 字段来修饰。其中add方法的notifyAll() 是为了唤醒处于wait状态的电梯。(呼应了前面elevatorReq.wait()

除此之外,isEnd属性和setEnd方法的作用是结束线程,表示这个请求队列不会再加入新的请求,第二次迭代的线程运行时序图也能体现这里的作用

第二次迭代

任务简析

这次任务是三次迭代中困难跨度最大的,增加了调度的需求,同时电梯增加了RESET的功能,故主要需要实现:

  • 调度线程(Dispatch)
  • 电梯RESET功能(Elevator)
UML类图

在这里插入图片描述

  • 结合任务,我在电梯线程和全局分发请求线程中间加入了调度线程,同时应用接口实现了三种不同的调度算法。RESET的实现在电梯类Elevator中不可见;InfoElevator 是实现评价调度算法时的托盘,后续调度类—性能提升篇细讲;Counter是计数器,用来确定线程结束条件,后文具体实现会讲。
具体实现
RESET的实现,基于强行阻塞

以下是本人的实现方法,后面还会简单介绍其他人好的实现方法。

先关注RESET前置操作,很多人是ResetRequest包装成和PersonRequest相同的请求放入到等待队列中再去让电梯响应,这个做法我认为好处在于可以同步乘客请求和重置请求,保证处理顺序的一致性。而我是使用了一种异步的方法:

//InputHandle.java
else if (request instanceof ResetRequest) {
  	ResetRequest req = (ResetRequest) request;
  	int elevatorId = nor.getElevatorId();
    Elevators.getElevator(elevatorId).initReset(req.getCapacity(), req.getSpeed());
  
//Elevator.java
public void initNormalReset(int maxRequestNum, double moveTime) {      //其实是第三次迭代的写法,区分了normalReset、doubleReset
		synchronized (requestQueue) {
				this.resetMaxRequestNum = maxRequestNum;
         this.resetMoveTime = moveTime;
         this.resetType = ResetType.NORMAL;
         requestQueue.notifyAll();
		}
}
  
private void normalReset() {
  	maxRequestNum = resetMaxRequestNum;
		moveTime = resetMoveTime;
  	outAllPassengerReset();
  	outAllRequestAndReset();
  	this.resetType = ResetType.NONE;
  	infoSync();            																					//同步信息给infoElevator
    }

private void outAllRequestAndReset() {      
  	synchronized (requestQueue) {
      	//print(RESET_BEGIN)
      	//将requestQueue中的请求放回总表重新调度
      	//sleep(RESET_TIME * 1000);
      	//print(RESET_END)
    }
}
  
//LookStrategy.java
boolean isReset = elevator.isReset();
if (isReset) {
		return RESET;
}  

通过以上代码就可以看出我的异步指的是电梯得到了RESET的通知是异步的,通过initReset改变电梯中resetType 这一属性,再通过LookStrategy策略类同步重置这一操作。

另外,initReset 取走了requestQueue的锁,这里的目的是为了后续notifyAll 可以唤醒wait状态的电梯。此处会很奇怪,因为initReset并未处理共享区,此处拿锁只为了唤醒,如果为了合理性,我觉得可以再设置其他的锁。将reset包装成乘客请求的方法应该依靠的是addPassengernotifyAll,本质是一样的。

至于强行阻塞,首先想这个问题:如何处理正在RESET的电梯的调度?互测中大家经常使用六个电梯同时RESET;或者将一个电梯先RESET的很慢,然后在最后一秒另外五个电梯同时RESET且输入所有的请求,如果处理不好这所有的指令都会分配给那一台提前RESET完很慢的电梯,这样子会超时。

为了应对以上的问题,我处理RESET的原则是:可以给RESET的电梯分配请求。但是又需要保证RESET的电梯输出”RESET-BEGIN“后不可以”RECIEVE“请求,这就用到了我上面代码中outAllRequestAndReset方法的设计,这个方法取走了requestQueue的锁,然后再进行输出RESET-BEGIN清空等待队列等操作,并且在电梯sleep的过程中也不释放锁,这就导致如果调度线程Dispatch要给该电梯addPassenger会被阻塞(因为addPassenger也需要requestQueue的锁),而我”RECIEVE“的输出在addPassenger方法中,这就保证”RECIEVE“的输出要么在”RESET-BEGIN“前(这样的”RECIEVE“会被打回总表重新调度);要么在”RESET-END“后(可能请求的分发在“RESET-END”之前,但被阻塞了,只能重置完成再输出“RECIEVE”。

但很多人应该发现,我这个设计会导致调度线程被强制阻塞”RESET_TIME“这么久,是这样的。但是这么设计保证了正确性,同时从全局分析,电梯RESET完分配的兴许会更合理。不过下面会介绍其他同学使用的buffer缓冲方法,可以规避调度线程被阻塞这个问题。

Buffer缓冲方法

也是本人的好室友zyt和我讲的,同时我发现很多人也都使用的这个方法,可见这并不是小聪明,小设计,反而缓冲的思想应用价值很高。

因为本人并未实现,只能借助他人的代码简单分析,但如果”后人“想要使用这种方法可以进一步深入思考并践行

//这段代码摘录于wxm同学的实现
public synchronized void addRequest(Request request) {
		if (request instanceof Person) {
      	if (inResetting) {
        		buffer.add((Person)request);
      	} else {
          	personRequests.add((Person) request);
        }
...
  
public synchronized void takeBuffer() {
		//将buffer中的乘客全部移动到personRequests中
		buffer.clear();
}

在输出“RESET-END”后调用takeBuffer将buffer中的乘客倾倒入personRequest,倾倒的过程中再输出“RECIEVE”,这样子处理可以规避上面阻塞调度线程的问题,同时也允许调度线程给RESET中的电梯分派乘客。

调度线程实现

相比第一次迭代,这次增加了“总表”globalReq 的设计,InputHandle线程将请求解析后放入到globalReq中,调度线程Dispatch 不断将globalReq中的请求分发给各个电梯线程的requestQueue中,故整个程序的运行时序图如下:

在这里插入图片描述

InputHandle接收标准输入流输入,并且不断解析请求放入到globalReq中,globalReq.add 唤醒Dispatch进行调度,将请求分配给各个电梯,elevatorReq.add唤醒电梯运行;当电梯处于RESET状态时,也调用globalReq.add来重新进行请求的分配;同时InputHandle解析完所有的请求且计数器清零的时候发出setEnd 方法,逐层结束线程。

计数器实现线程结束条件

因为globalReq 会有回流,故线程结束的条件就不能简单的是InputHandle简单处理完所有的输入请求,而是所有的请求都已经输入到总表且电梯不会再有RESET指令,这时才能结束调度线程。而我设计的更加保险,我通过Counter维护所有的乘客数量,当Counter清零时即所有乘客都送达目的地,再结束所有线程(感觉会造成一点点程序运行的耗时,但微乎其微啦)

//Counter.java
public class Counter {
    private final AtomicInteger totalRequestCount = new AtomicInteger(0);

    public synchronized void increment(int count)
    public synchronized int getCount()
    public synchronized void decrement(int count)
}

当然也可以简单处理,只维护一个passengerNum变量(yt貌似是这么做的),但是一定要做这个考量,不然调度线程提前结束,globalReq的回流无法处理

第三次迭代

任务简析

这次任务相比第二次迭代的改动并不多,因为架构已经确定,更多的是在此基础上拓展双轿厢电梯,使其适配已有的调度算法并可正确运行,故主要需要实现:

  • 双轿厢电梯安全运行(TransferFloor)
  • 双轿厢doubleReset(Elevator)
UML类图

在这里插入图片描述

放大粉色部分为新增,只增加了TransferFloor换乘楼层类,并且可以在Elevator类中创建新的电梯(也就是他的双轿厢),二者共用一个TransferFloor来达到互斥,不同时进入换成楼层的需求

具体实现
TransferFloor换乘楼层互斥

这个方法也是学习的讨论区中某同学分享的思路:即让两个轿厢共享一个对象,来互斥进入,同时做一些硬性规定,如电梯进入换成楼将人全部放出后立刻离开换乘楼层。

//TransferFloor.java
public class TransferFloor {
    private AtomicInteger floorNum;
    private State state;

    public synchronized void setOccupied() {
        while (this.state == State.OCCUPIED) {
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        this.state = State.OCCUPIED;
    }

    public synchronized void setEmpty() {
        this.state = State.EMPTY;
        notifyAll();
    }
}

//Elevator.java
private void transfer() {
		direction = doubleType != 'A';
		open();          //不事不登换层楼,肯定是要接人或者放人,直接开门
		if (!passengers.isEmpty()) {
				outAllPassenger();
		}
		inPassenger();
		close();
		move();	        
		this.transferFloor.setEmpty();
}

private void move() {
		...
		if (currentFloor == transferFloor.getFloorNum() && resetType == ResetType.NONE) {
				transferFloor.setOccupied();
		}
  	...
}

transfer 方法是LookStrategy返回电梯状态为TRANSFER时调用,保证电梯在换乘楼工作完后立刻离开

doubleReset的实现

我觉得这个是这次迭代最难的点,依旧沿用我之前先initReset导致了很多问题,比如信息更改的时期,以及做严重的是和调度线程的交互,因为之前调度类可以给RESET-BRGIN的电梯派遣请求,但是RECIEVE的输出要延后。可现在如果给某个轿厢派遣请求,但他不一定能完成这个请求,比如这个请求乘客的出发楼层不在该电梯的运行范围内,所以要做好电梯属性的设置以及调度算法的设计

本人这里只简单说明下自己的设计,但是自己设计的真的很烂,有很大的优化空间,‘’后人‘’可以参考这个方向然后实现更好的设计

//Elevator.java
public void initDouble(int transferFloor, int maxRequestNum, double moveTime) {
  	synchronized (elevatorReq) {
      	this.resetType = ResetType.DOUBLE;
      	this.transferFloor.setFloorNum(transferFloor);
      	this.resetMaxRequestNum = maxRequestNum;
      	this.resetMoveTime = moveTime;
      elevatorReq.notifyAll();
    }
}

private void doubleReset() {
  	//A init reset
  	maxRequestNum = resetMaxRequestNum;
    ...
    this.infoSync();
		
  	//reset and outReq
  	outElevatorReqAndReset(true);

  	//A finish reset
  	this.printId = elevatorId + "-A";
  	this.resetType = ResetType.NONE;

  	//B finish reset
    Elevator elevator = new Elevator(elevatorId + 6);
    elevator.setPrintId(elevatorId + "-B");
    elevator.setCurrentFloor(transferFloor.getFloorNum() + 1);
    ...
		elevator.infoSync();
		
  	//add B
  	Elevators.addElevator(elevator);
  	elevator.start();
}

所以我的电梯在进行doubleReset的过程中是可以接收它可以处理的请求的,但如果当前的Elevators中的电梯都不能处理某个请求(比如六个电梯都在doubleReset,这时候还没有B电梯加入进来,11楼出发的人1-6号A电梯都无法满足),调度类需要等待新的电梯加入。


多线程—线程安全篇

多线程函数的使用

这里在第一次迭代中特别困惑我,因为对于一个第一次接触多线程的人来说,许多陌生的函数,线程的各种状态之间的转换,多线程的不可控都让我不敢写代码。故这里只简单罗列下当时自己学习线程函数时候的心得:

wait()

  • 如果调用某个对象的 wait() 方法,它会使当前线程等待并释放该对象的锁。
  • 在这种情况下,需要确保在调用 wait() 方法之前已经获取了对象的锁,否则会抛出 IllegalMonitorStateException 异常。
  • 在这种情况下,只有持有该对象锁的其他线程才能调用该对象上的 notify()notifyAll() 方法来唤醒等待中的线程。
  • 上面持有锁的方式有方法用synchronized字段修饰或者代码段用synchronized修饰

synchronized字段

  • 用该字段修饰了方法或者代码段说明调用这个方法的线程获得了对象的锁,其他线程中的需要获得该对象的锁的方法在该方法或代码段执行完之前,会被阻塞住(我认为相当于wait),等待notify将其唤醒

notify()

  • 即唤醒线程等待队列中的线程,我听说所有的notify都可以用notifyAll来代替(不敢苟同,但我确实使用的都是notifyAll),但二者确实都之能唤醒一个线程等待队列中的线程

上面一部分只是在oo课程中的介绍很少,更多的在于自己查阅资料,阅读源码去学习,并且更多的是在实践中掌握(写了一遍代码之后我多少有点理解,但也不敢讲说很熟练),另外os理论课中也有对线程同步互斥的介绍(信号量、管程等),可惜U2结束了它才讲,有点倒反天罡。

线程安全的实现

从全局来看本人代码中涉及线程安全的部分:

  • synchronized修饰的方法/代码段:

    • RequestQueue中add、del、setEnd等方法
    • TransferFloor中的setOccupiedsetEmpty 方法
    • InfoElevator中的getCost、set方法

    以上三个会发现都是共享对象,1、3还是生产—消费者模式中的托盘

    • Elevator 在从等待队列中取走乘客或将等待队列回流时需要锁住requestQueue

    • Dispatch在向电梯等待队列中加入乘客时需要锁住requestQueue

  • wait方法:

    • InputHandle等待Counter被清零
    • Dispatch等待globalReqnotify
    • Elevator等待requestQueuenotify
    • TransferFloorsetOccupied方法中等待setEmptynotify
  • notify方法:

    • RequestQueue 中add、setEnd方法执行后notifyAll
    • TransferFloorsetEmpty执行后notifyAll
    • Counterdecrement 执行后notifyAll
    • ElevatorinitReset结束后执行notifyAll

此外我的Counter类使用AtomicInteger维护,Elevators静态类用ConcurrentHashMap<Integer, Elevator>来维护,读写锁我没有使用,但是读写锁更灵活,也值得考量

其实这些线程安全方法的设置很灵活多变,我上面的赘述只是方向上的提供思路,具体实现细节也复杂多变。但需要保证自己的程序不出现轮询和死锁,下面会对这两个典型bug做介绍

轮询

轮询其实谈不上是bug,但是会导致cpu占用时间过高,造成CTLE。这个一般是由于多次重复的while等待条件的到来所导致的,线程方法中的waitsleepsynchromozed修饰方法取锁失败等都会让线程挂起而不占用cpu,故在走查的debug方式下,应该更加关注while语法的出现部分,同时有人提出可以使用IDEA自带的性能分析工具来分析热点函数,本人并未使用过,不做评价。

死锁

死锁一般是由于两个线程都需要前后两次取锁,但二者取锁顺序不同,导致都卡在了第二次取锁的地方,这时候二者都被挂起,线程未结束,最后会RTLE。(当然也可能是三者或以上循环取锁导致取不到)

针对死锁的走查,本人是观察所有的synchromozed修饰的方法和代码段,观察全局取锁顺序是否保持一致,或者如果发生了不一致,可否安全的结束synchromozed修饰的方法和代码段。

有人讲说比如全局取锁顺序都是先A后B,但是有一个地方只能先B后A,那么可以在BA的外面再套一层A,即ABA的取锁方式,虽然听着很离谱,但我觉得有一定的可行性(未尝试过,不敢妄下定论)

当然de死锁的bug的时候复现死锁是非常重要的,使用调试模式,一旦程序不输出了,按暂停观看各个线程的运行情况,当然死锁的复现不是百发百中的,这种交给运气的事确实有点逆天。

生产—消费者模式

其实前面已经讲到了RequestQueueDispatchElevator生产消费的托盘;globalReqInputHandleDispatch的托盘;最后新增的InfoElevatorElevatorDispatcher的托盘。

其实这么设计出来有点奇怪,原本的需求是调度类需要得到电梯的信息,需要去电梯中获取(调用电梯的get方法),但是我害怕会引发线程不安全,故设计了InfoElevator,电梯将信息写入这里(调用InfoElevatorset方法),然后调度算法对InfoElevator做操作,这样又建立起来了一个生产—消费的桥梁。

不过实践下来觉得除了体系结构上更好看(也没好看到哪去),没有什么实质性的提升,不如调度算法需要对电梯做评分的时候现取现用(本人使用的调度算法是评价算法)


调度类—性能提升篇

这里便是在保证安全性的前提下 ,拉高了U2可玩性上限的地方。本人实现的调度算法是评价算法(一种低投入中回报的算法),相比于影子电梯的高投入高回报,各有千秋吧。同时我也会介绍一些好玩的小设计(不少是听来的)

量子电梯

我对量子电梯的理解是不死板的使用sleepwait 去让电梯等待刚刚好的时间,而是在这些时间内做很多特判,来思考是否有更好的运行方式。(应该是电梯运行策略的范畴)

比如下面这个例子(摘自wxm分享):

在这里插入图片描述

说白了就是对sleep|wait的时间做压缩,使其“不知所处”或者“耗时计算”的时间纳入到运行的时间里

本人只是简单介绍,更加详细优秀的介绍如下:

wxm:影子电梯+量子电梯实现过程实录

bzh:等一会再关门——一种优化的量子电梯策略

弹射起步

这个说法来自于肖老师的博客(肖老师好像还是听另一个zyt说的)

  • 因为电梯运行时间是根据两个arrive之间的时间来考虑的。
  • 所以每个电梯的第一个arrive,它没有参照物,它不需要sleep

影子电梯

该算法是我觉得目前来讲U2的最高水准,由于本人尝试且以失败告终了,想学技术的人请直接跳转到友链吧

wxm:影子电梯+量子电梯实现过程实录

这篇经验分享真的被我吹爆,从知识的灌输,体会的分享各个方面都让人眼前一新

(没有OO平台账号是不是看不了啊,不知道咋办不知道咋办)

那我下面就赘述下我是被什么困难所挡住了:

首先我一开始理解的影子电梯就出问题了,我以为影子电梯只模拟所有电梯接到这个请求的最短时间,但后来得知事实上还要模拟运送这个乘客,计算将其送到站所需的最短时间。所以看起来最好的做法就是完全模拟电梯运行的逻辑,将电梯运行中sleep|wait都换乘costTime+=

所以影子电梯两大难点可以分为:克隆环节模拟环节

克隆环节要注意时间戳的处理,要考量线程安全……很多维度都需要去注意,同时还要思考自己要做的是一个十分细颗粒度的模拟还是一个允许一定误差的模拟,有时颗粒度越细也不一定会得到很好的效果。

好了,no do no bb,上面的话只是庸人自扰,倔强的瞻仰下这个高投入高回报的方法,下面的哦评价调度是自己真正使用的算法

评价调度

评价调度就是提取电梯当时的一些状态特征,如:在哪层楼、运行速度、最大乘客量、等待队列中的人等等,用这些指标来做一个评价,电梯会得到一个评分,让评分最优的电梯去接这个乘客。

故我们可以得到很多结论:

  • 乘客起始楼层和电梯当前楼层距离越短越优(注意方向啊,上行的2楼电梯和1楼的乘客距离可不只是1层)
  • 候乘表中的人和电梯里的人人数越少越优
  • 运行速度越快越优
  • 最大载客量越大越优

故可以粗略的建模成一个公式:

//InfoElevator.java
public synchronized int getCost(Passenger passenger) {
        if (!canTransfer(passenger.getFrom(), passenger.getTo())) {
            return Constants.INT_MIN;
        }
        if (passengers.size() >= maxRequestNum && passenger.getFrom() == currentFloor) {
            return Constants.INT_MIN;
        }
        if (elevatorReq.size() >= maxRequestNum * 2) {
            return Constants.INT_MIN;
        }
        double estimate =  ((33 - getDistance(passenger) - passengers.size() * 10 - elevatorReq.size() * 10 + maxRequestNum * 1.3 - moveTime * 17) / (getDistance(passenger) * moveTime));
        return (int) estimate;
    }

第12行的公式即根据各个指标的性质决定正负号、分子分母、系数大小,这个方法的可玩性在于有优化空间, 可以于影子电梯对拍来调节参数,或者是自回归去优化性能。

同时前面几行我也设计了返回int最小值的做法,当所有的电梯都是最小值的时候,那就等一会再去做评价(其中第一个指标canTransfer是对双轿厢做考量,保证请求电梯一定能接送到

自由竞争

时代留下的产物,由于这学期课程组有意ban掉这种做法,导致这种低投入高回报的调度算法被淹没在了历史的长河中(肖老师认为可以用增加电梯磨损程度这一性能指标来限制自由竞争,但它都被ban了,就放过他吧)

粗略一点的思路就是当得到一个请求之后,让所有的电梯都去运行,争夺这一个请求,谁先抢到就归谁,自由竞争顾名思义。所以就造成了所有的电梯会乱跑的情况(今年的”RECIEVE“限制了这种做法),但是听说效率可以比肩影子电梯

摩天轮调度

本人在U2之前的胡思乱想,当然和自由竞争有着类似,都是电梯乱跑(但这个不乱),被”RECIEVE“限制ban掉了

粗略思路就是让电梯遍布在所有楼层中(1-11层往返就是22层),那么隔三四层设置一个电梯,让电梯像一个摩天轮一样循环运行,乘客请求到来之后搭乘离他最近的摩天轮轿厢,如果当前电梯已满人,那么就等待下一个摩天轮轿厢。

这样做真的十分费电,而且很无厘头,但是细想他能保证一个很平均的效率,抛去特殊样例的情况下效率应该还可以。


结语

其实其中有很多人尽皆知的点,甚至我的很多设计在大佬们看来很幼稚,但我写了这么多,是想对我这个不那么完美的U2画上一个句号。有人说U2结束,好玩的OO也就结束了。那么既然来过,总要留下一些痕迹。人生若只如初见,人生亦只如初见。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值