在本章的三次作业里,每次作业我都有一个主题,分别是:托盘型共享数据、单步电梯运行优化、多部电梯运行优化,因而电梯优化实际是第二、三次作业。虽然后两次作业从性能分上看做得还不错,但阅读其他大佬博客,我深深地体会到鄙人朴素拙劣的调度算法能够取得这样的分数实属幸运,因此,对于这篇博客,我将只简略地介绍一下自己的调度算法,把重点放在关于电梯调度架构的思考上。
电梯调度算法
-
单电梯调度:对于每部电梯,我的调度算法都是朴素的LOOK算法,并且当休眠的电梯同时收到了可上可下的请求时,其会向从接人运行路程较短的方向运行。
-
多电梯调度:本次作业的多电梯各数据参数已经详细给出,而且调度效果不好标准化衡量、数据稀少,不利于使用多参数的预测表达式作为决策依据,因此我选择的策略是设计基于人工先验知识的调度算法,具体来说,我的调度策略如下:
-
能够直达的任务必须直达
- 策略一:首先,C电梯能执行则给C电梯,其次,B电梯能执行的给B电梯,最后,A电梯能执行的给A电梯。
- 策略二:负载均衡,在ABC电梯中选择能够执行任务的电梯,并将任务委派给总量最小的电梯。
-
不能直达的任务至多拆解为两个
根据Excel表格的分析,在基于直达任务必须直达的前提条件下,不可直达任务的起/终点集中于15层以上、-3层和3层,对于15层以上楼层,调度器将会把子任务目标定为15层,对于-3层和3层,调度器会把子任务目标定位1层。
以上策略最终合成时很简单,但其实是对各类起终点情况分类考虑后综合起来形成的结果,我使用随机生成数据进行测试,数据量50条,每条指令投放间隔时间为[0.0, 1.0],运行组数20组:
策略 平均调度时间 基准调度:直达任务随机分配+非直达任务均放置于1层 58.2542 S 对比调度1:直达策略一+非直达拆解 51.2311 S 对比调度2:直达策略二+非直达拆解 52.3192 S 可以看出优化后的固定调度有一定的提升,且在强测中也有较好效果,不过根据周围同学的反馈,固定调度还是明显有性能瓶颈的,要想进一步提升,还需要在调度算法和模式上进行大幅度改进。
-
关于多电梯调度架构的思考
基于任务可拒绝的分布式调度结构
经过后期思考和研讨课的讨论,同学们对电梯调度算法主要分为两类:
- 分配式:任务由主调度器进行拆解并指定执行电梯,电梯行为被动。
- 优点是能够从全局上分析,找到最合适的执行电梯。
- 缺点则是对每部电梯进行动态仿真评估时难度较大(电梯内部数据不安全、评价标准难定等……)。
- 抢占式:删去主调度器,由电梯根据自身运行状态从请求列表中抢夺合适的任务,电梯行为主动。
- 优点是减少了动态仿真评估的复杂度(例如请求列表按照楼层分布,先到的电梯先得,免去了许多对运行的预测)。
- 缺点是缺乏全局分析,存在任务分配不均衡的可能性,且对于不可直达的任务,从电梯线程的角度拆分任务比较困难。
本着将两种调度优势结合(和稀泥)的考虑,提出了一种“任务可拒绝的分布式调度”结构,结构图就像这样:
具体来说,框架主要改编自“分配式”的架构,新增了由电梯线程向请求池反馈任务完成情况和退回拒绝的任务。
-
主调度对任务的分配显然是调度,而电梯对某个任务的拒绝并释放也是另一种角度的调度,两个阶段对任务的影响从而形成了分布式的调度结构。分布式的调度方式一定程度上结合了我在上文中所提到两种方法的优势,即主调度器擅长全局地指派任务,而电梯擅长动态仿真分析接受或拒绝任务。
-
在此举一个任务可能流动的例子:
- 主调度器首次取出:根据一些全局策略(最好是基于先验知识的、依赖于静态数据的),把任务首先支配给电梯X。
- 电梯X分析任务:电梯X从“任务信箱“读取任务,结合自身动态运行情况(容量、速度、方向、楼层等),决定接受/拒绝任务(评估算法应该设计成“能拒绝的尽量拒绝”,因为拒绝了有可能再次收到)。
- 主调度器再次取出任务(任务被拒情况下):分析拒绝者身份和拒绝理由,对任务进行重新拆分、重新分配或者向电梯X委派不可拒绝的任务。
-
此外,在此例举两个本架构中的关键类:
1 第一,是获取任务的请求池
RequestPool
,请求池是一个托盘类,生产者是输入线程和电梯线程,消费者是主调度器,由于请求池对调度器是透明的,因此主调度器的任务获取与“分配式”方式完全一致,有关请求池的守护者模式等实现请参考第二章的总结作业的心得与体会。public class RequestPool { // single item mode private RequestList() {}; public static RequestList getInstance() {} // 由输入线程确定输入结束 public synchronized void setNoMoreRequest() {} // 由输入线程投放新的乘客任务 public synchronized void createNewPassenger(Passenger p) {} // 由电梯线程反馈/拒绝乘客任务:反馈的任务,乘客p的当前楼层已由电梯改变; // 拒绝的任务,乘客p的当前楼层不变,且附带更多拒绝信息。 public synchronized void taskFinishFeedback(Passenger p) {} public synchronized void taskRejectFeedback(Passenger p) {} // 有主调度器读取在请求池中的任务 public synchronized PersonRequest getPassenger(){} }
2 第二,是乘客类
Passenger
,由于任务分拆中转点的设立和任务拒绝时记录详细拒绝信息的需求,课程组提供的PersonRequest
功能已不足,为此需要新建乘客类满足功能需求:public class Passenger(){ // 乘客最初的起点与目的地,实例化后不可更改 final private int oriFloor; final private int desFloor; // 乘客当前所在楼层和子任务的目标楼层 private int curFloor; private int tarFloor; // 此乘客是否可以被电梯所拒绝 private boolean unRejectable; // 电梯拒绝理由的记录表 private HashMap<Elevator,RejectRecord> rejectMap; public Passenger(PersonRequest p); // 改变和读取当前所在楼层和子任务目标楼层 public void setCurrentFloor(int floor); public int getCurrentFloor(); public void setTarFloor(int floor); public int getTarFloor(); // 判断乘客是否已经到达终点 public boolean isArrive(); // 设置和读取乘客是否可被拒绝 public void setUnRejectable(boolean flag); public boolean isUnRejectable(); // 新增或读取对应电梯的拒绝理由 public void addRejectRecord(Elevator e, RejectRecord record); public RejectRecord getRejectRecord(Elevator e); }
3 第三,是拒绝理由类
RejectRecord
,此类所保存的内容随主调度器算法差异而有很大差异,目前大致想到的有:拒绝搭载的路段、拒绝依据的主要理由、拒绝时的楼层方向容量数据(方便主调度器在电梯都拒绝时强制执行。)
优先队列和楼层类的电梯任务维护
对于每部电梯,其内部任务维护有着两种主流的方式——队列和楼层类:
1 队列实现:在队列中,电梯每通过一层即扫描队列中的任务,并根据电梯电梯已有任务和列表任务选择合适的方向运行。不过,如果每次都完整扫描一次队列固然性能开销很大,为此一种优化的方式是实现Comparator接口维护优先队列,按照既定逻辑(如Look)排列当前任务,电梯每到一层仅需要查看队头任务即可。
//伪Look算法的实现
public static Comparator<Passenger> passComparator = new Comparator<Passenger>(){
@Override
public int compare(Passenger p1, Passenger p2) {
// 优先级1:在电梯运行方向的路上,内部按照与电梯距离排序。
// 优先级2:不在电梯运行方向的路上,内部按照与电梯距离排序
}
};
2 楼层类实现:在楼层而楼层方法则尽可能与现实乘电梯相似,电梯每到一层就向楼层类请求,查看存在的接送任务。
我采用的是此类方法,但在在实现时由于楼层类功能过于强大且实现结构不佳,甚至都没有显示地维护电梯内部的乘客id,因此在后期基于look算法优化和仿真评估的实现上遇到很大的阻碍,耦合过强。因此,建议在使用楼层类实现时:注意楼层类功能与电梯功能的明确划分、基于队列思想维护电梯内的“送达”乘客、动态仿真交给电梯实现楼层类只提供不复杂的参考信息。
说道最后,写OO是一个”逐渐后悔”的过程,不过勤思勤练下后悔之处会越来越少,以上的OO优化总结便结合了本章作业中我的后悔之处,其中有的想到的具体实现框架,有的地方依旧难以平衡解决。欢迎各位有思路的参与讨论,谢谢~