269
社区成员




第一次作业参考了课上实验的代码,采用生产者消费者模型
设计出Main、PassengersQueue、ProcessingQueue、InputThread、Scheduler、Elevator六类
其中InputThread、Elevator、Scheduler为线程类
InputThread处理输入
Scheduler分配请求
Elevator处理请求
处理流程如下
需要读写的类:PassengersQueue 、ProcessingQueue
考虑到同步逻辑并不复杂,使用synchronized修饰以上两类的方法,可以很好地保证操作的原子性
本次作业的乘客请求指定了乘坐的电梯,无需赘述
将分配至ProcessingQueue的请求分类为 upQueue 和 downQueue
在Elevator中设置变量 state ,有 MOVING_DOWN 、MOVING_UP 、IDLE 三种状态
这种策略可以保证elevator在往返时,满足电梯中的乘客请求,同时捎带尽可能多的乘客,还可以尽量避免出现极端情况下某些乘客被“饿死”的情况
为了优先满足高优先级乘客请求,将upQueue、downQueue设置为PriorityQueue
第二次作业并未指定乘客请求的乘坐电梯,同时,新增了SCHE请求
需要修改Scheduler类中的分配策略,增加Elevator中对SCHE请求的处理
此次作业,笔者将 synchronized修饰 更改为 ReadWriteLock ,希望能允许多个读操作并发执行,提高运行效率,然而对性能几乎没有任何影响
笔者设计了一个基于电梯状态、电梯负载、电梯楼层、待处理队列的估分分配策略
略微进行了参数调整,性能强于随机分配
private int evaluateElevator() {
// 1. 方向适配性(优化运行时间 & 耗电)
if (...) {
if (...) {
// 方向一致且顺路
} else if () {
// 方向一致但逆路
}
} else if (...) {
if (...) {
// 方向一致且顺路
} else if (...) {
// 方向一致但逆路
}
} else if () {
// 空闲电梯更高基础分
}
// 2. 距离评分
// 3. 负载评分
if () {
// 过载惩罚
}
// 4. 分配评分
}
由于估分分配策略只能避免 某些情况下的最坏分配 ,而无法选择 尽可能优秀的分配
笔者尝试在 运行策略 上下手
考虑这样一种情况假如在低楼层之间(如B1到F2)有大量低优先级乘客请求,高楼层间如(B4到F7)有大量高优先级乘客请求
在先前的运行策略中,电梯往返时,低优先级乘客会先占满了电梯,导致后续无论多高优先级乘客都无法再进入电梯
直到低楼层间的乘客请求处理完后,才会开始处理高优先级乘客
故而,笔者构思这样一种策略当电梯满员时,一旦当前楼层存在同向优先级高于电梯中最低优先级的请求
将电梯中最低优先级的乘客踢出电梯,进入PassengersQueue
再将高优先级乘客接入电梯
此种运行策略可以保证,在所有请求的总运行路程不会延长的前提下
能够优先处理高优先级请求的情况
还能够避免某些情况下,过量的请求分配至某一电梯,导致运行时间过长的问题
不过也可能存在由于大量优先级差距小的请求频繁开关门切换导致的耗电量激增的问题
可以考虑为切换乘客的优先级差距设置一个阈值 增加存在有人上下电梯时才进行切换的判断
由于本次作业新增了可以取消receive请求的Sche请求,以及电梯可以把乘客踢入PassengersQueue的功能
参考课上实验代码设计的终止判断
考虑以下情况
在输入线程终止,调度线程终止时,电梯线程可能由于执行Sche请求或者将乘客踢入PassengersQueue,导致PassengersQueue不为空
而此时调度线程已经终止,此时PassengersQueue中的请求无法再分配,无法再处理乘客请求
笔者选择,在Main类中新增static变量,类型设置为AtomicInteger,保证对变量操作的原子性
当InputThread接受到乘客请求时,该变量自增;当Elevator处理完乘客请求,该变量自减
直到该变量为0,Scheduler线程才终止
此种方法可以避免死锁,也算是一种取巧的办法
代码如下
private static final AtomicInteger number = new AtomicInteger(0);
public static int getNumber() {
return number.get();
}
public static void addNumber() {
number.incrementAndGet();
}
public static void subNumber() {
number.decrementAndGet();
}
然而,由于此时Scheduler线程和Elevator线程互为生产消费者
此种设计会出现Scheduler线程的轮询,故而在Scheduler中加入sleep语句
本次作业新增UPDATE请求,将两部电梯合并为双轿厢电梯
双轿厢电梯的两个轿厢,仍然是电梯线程,其处理逻辑并未改变,故而
可以将轿厢看作更改了运行范围的初始电梯,同时增加了避免同时占据分隔目标楼层的要求
如此一来,修改接受Update请求的初始电梯的运行范围,便可以避免额外增减线程带来的额外开销,直接使用原电梯线程
同时无需对Scheduler进行修改,也无需新增ProcessingQueue对象
为了UPDATE请求的两部电梯能够同时完成UPDATE请求,以及避免同时占据分隔目标楼层
可以在Elevator执行UPDATE请求时,在两部电梯中新增一个共用锁lock
架构设计如下
UPDATE同步完成的实现
Object lock = ...
synchronized (lock) {
//更改状态为READY
if (//当配对电梯READY) {
//...
//唤醒
} else {
//等待
}
其逻辑如下
避免同时占据分隔目标楼层的实现
private void move() {
if (//静止状态) {
//如果占据分隔目标楼层则唤醒
return;
}
// 检查下一层是否为当前电梯的分隔目标楼层
if (//存在配对轿厢) {
if (//下一层为分隔目标楼层) {
Object lock = ...
synchronized (lock) {
Integer occupiedBy = ...
if(//分隔目标楼层已被配对轿厢占用) {
if (//配对轿厢线程不为等待状态) {
//等待
}
if (//配对轿厢线程为等待状态) {
//配对轿厢移动
}
}
// 标记分隔层为当前轿厢占用
}
}
}
// 执行移动
// 更新占用状态
}
除了以上两类,其他类几乎没有变化
由于6个Elevator线程本质上并没有不同
为了提高同样样例下多线程运行的压力,也为了更方便观察线程运行
我在Scheduler中将分配电梯强制划定为1、2
集中对其进行数据轰炸
然而,此种方式也有明显问题
在第三次作业中,由于我在测试时将分配电梯强制划定为1、2
并未发现将锁设置为分隔目标楼层,导致的一旦双轿厢电梯分隔楼层相同,就会出现双轿厢电梯共用一把锁,进而导致输出错误的bug
还是得完整测试,不能想当然(/(ㄒoㄒ)/~~)
由于本次作业主要设计都是参考的课上实验代码,从第一次作业到第三次作业的架构并没有什么变化
好的层次化设计可以极大减少迭代时间,但是一旦想要使用其他性能优化方法(如影子电梯)就很难,需要修改非常多的地方
不过,由于大量修改都集中于Elevator类,导致其代码十分臃肿
在其他类只有寥寥数十行的时候,Elevator类以一己之力干到了将近500行,
其中存在大量的冗余代码,以及一些不优雅的判断语句
这大概也是我难以使用其他性能优化方法的原因吧(