BUAA OO 第二单元总结:自由竞争在横纵电梯问题上的扩展做法
OO第二单元总结:自由竞争在横纵电梯问题上的扩展做法
第一次作业
题目大意
五座楼初始各有一个竖直方向电梯,乘客起始楼座和目的楼座相同。
算法设计
纵向电梯调度:Look算法。具体实现为:
- 移动策略:初始电梯运行方向为上,请求到来之后判断与电梯运行同方向是否有请求(例如如果电梯运行方向为上,则检查当前楼层之上是否有请求),如果有,电梯继续向相同方向运行,否则转向。
- 捎带判断:与电梯运行方向相同即可捎带
无其他优化。
强测结果: 99.0525 。
架构设计
-
多线程方面,采用生产者-消费者模式,其中
- Data 对应
Person
。 - Producer 对应
InputHandler
。负责处理输入,与RequestTable
的交互有 add、close(结束信号) - Consumer 对应
Elevator
。负责把乘客运送到目的地,与RequestTable
的交互有 remove、query - Channel 对应
RequestTable
。使用的单一集合存放所有未到达目的地且未上电梯的人。为保证线程安全,对RequestTable
中所有方法使用synchronized
关键字进行互斥处理。
- Data 对应
-
电梯运行上,使用策略模式,电梯线程类只负责执行当前状态下的操作(状态有
REST/ARRIVE/MOVE/OPEN/CLOSE/END
),而电梯所含的Strategy
则根据Elevator
和RequestTable
给出状态转换的方案,状态转换用有限状态机表示如下(开门条件为Look算法的捎带判断):
- 输出类用单例模式封装。
类图如下:
第二次作业
题目大意
五座楼初始各有一个竖直方向电梯,可增加横向/纵向电梯,乘客起始楼座和目的楼座相同 或者 乘客起始楼层和目的楼层相同。
算法设计
纵向电梯调度:Look算法,同第一次作业。
横向电梯调度:
- 移动策略:电梯默认移动方向为顺时针。加入新乘客时首先计算乘客横向移动最短距离方向(顺时针或逆时针)。如果电梯不为空,运行方向不变。如果电梯为空且有和电梯运行同方向的乘客,电梯运行方向不变。
- 捎带判断:不单独判断方向,能上则上
多电梯调度:采用自由竞争,不单独设置调度器
无其他优化。
强测结果: 98.2903
架构设计
在第一次作业基础上:
-
多线程方面,采用读写锁替代
synchronized
关键字,以进行更精细的控制(实际对运行时间无影响) -
电梯运行上,简化了第一次的状态转移,去除了
ARRIVE
和CLOSE
状态。同时,为避免多电梯调度导致的电梯产生未知操作,在电梯移动相关操作中加入了边界判断
-
电梯类采用工厂模式,可生产横向电梯和纵向电梯。
-
更新策略模式实现,为横向/纵向电梯配置独立的运行策略,接口功能为 更新电梯状态(
newStatus
) 和 查询应该捎带的乘客(personToPick
) -
乘客类采用工厂模式,可生产横向移动的乘客和纵向移动的乘客。
类图如下:
第三次作业(自由竞争在横纵电梯问题上的扩展做法)
题目大意
五座楼初始各有一个竖直方向电梯,1层初始有一横向电梯,可增加横向/纵向电梯,电梯信息可配置,对乘客请求无特殊限制。
算法设计
自由竞争在横纵电梯问题上的扩展做法
自由竞争的优势在于可以考虑到电梯的容量和速度(因为容量大和速度快的电梯更有竞争力),同时不需要新增加代码。从一名乘客的角度来看,当存在多条可选择的目标路径时,他可以选择先乘坐横向电梯或者先乘坐纵向电梯,所以无论横纵电梯都可以竞争这个请求,而竞争到的电梯仅需考虑该乘客在对应方向(横向或纵向)的终点即可。
例如请求1-FROM-A-10-to-C-1
,此时在第 1 层第 10 层有中转电梯,这个人可以选择乘坐横向电梯到达 C10 或者乘坐纵向电梯到达 A1 ,此时位于 10 层的横向电梯和位于A座的纵向电梯都可以竞争这个请求。
示例代码如下:
class Person {
private HashMap<Direction, Integer> directions; // 储存乘客所有可能的下一步方向和对应目的地
// 规划路线(注:在新乘客加入/新电梯加入/乘客下电梯但未到目的地时调用该方法)
public void resetDirections() {
directions = new HashMap<>();
// 楼层相同或楼座相同,下一步方向唯一
if (fromBuilding == finalBuilding) {
directions.put(floor2Direction(fromFloor, finalFloor), finalFloor);
return;
} else if (fromFloor == finalFloor) {
if (elevatorsMsg.ableToGetOn(fromFloor, fromBuilding, finalBuilding)) {
directions.put(building2Direction(fromBuilding, finalBuilding), finalBuilding);
return;
}
}
// 出发点可中转,则可在出发点乘坐横向电梯
if (elevatorsMsg.ableToGetOn(fromFloor, fromBuilding, finalBuilding)) {
directions.put(building2Direction(fromBuilding, finalBuilding), finalBuilding);
}
// 终点可中转,则可乘坐纵向电梯先到终点层
if (elevatorsMsg.ableToGetOn(finalFloor, fromBuilding, finalBuilding)) {
directions.put(floor2Direction(fromFloor, finalFloor), finalFloor);
}
// 起点和终点无有效横向电梯,则寻找可行的中转层
if (directions.isEmpty()) {
findTransport(); // 实现略
}
}
// 捎带判断
public boolean pickDirection(Direction direction, Elevator e) {
if (direction == Direction.UP || direction == Direction.DOWN) {
return directions.containsKey(direction);
} else if (directions.containsKey(Direction.LEFT)) {
return e.getMask().accessible(this.fromBuilding, directions.get(Direction.LEFT)); // 可达性判断
} else if (directions.containsKey(Direction.RIGHT)) {
return e.getMask().accessible(this.fromBuilding, directions.get(Direction.RIGHT));
} else {
return false;
}
}
private Direction floor2Direction(int from, int to) {
return (from < to) ? Direction.UP : Direction.DOWN;
}
private Direction building2Direction(int from, int to) {
return ((to - from + 5) % 5 <= 2) ? Direction.RIGHT : Direction.LEFT;
}
}
此外,分析并更正了第二次作业性能比较差的部分:
-
第二次作业的横向电梯的移动策略表现比较糟糕。横向电梯新的移动策略为:若电梯为空则以最短路径去接最近的乘客,若电梯不为空,则判断电梯内乘客从目前电梯位置到目的地最短距离方向是否与电梯运行方向一致,若不一致,则改变电梯运行方向。
-
应当避免自由竞争中电梯开门却无人上/下的问题。解决方法为将 捎带查询 和 移除候乘表中的乘客 封装成一个原子操作remove,返回值为能够捎带的乘客,这样策略类在开门判断时直接调用remove方法,如果 电梯内有乘客要下 或 能够捎带的乘客不为空 ,则开门(原理就是在开门判断时将乘客分配给了电梯)。
示例代码如下:
class RequestTable { public ArrayList<Person> remove(Elevator e) { lock.writeLock(); ArrayList<Person> success = new ArrayList<>(); // 储存能够捎带的乘客 // 查询 for (Person o : persons) { if (o.ableToGetOnElevator(e)) { if (e.ifFull(success.size())) { break; } success.add(o); } } // 移除 for (Person o : success) { persons.remove(o); } lock.writeUnlock(); waitRoom.notifyAllInWaitRoom(); return success; } } class Elevator { protected void openDoor() { output.printMsg(String.format("OPEN-%c-%d-%d", getBuildingName(), this.floor, this.id)); personOut(); personIn(); sleep(CLOSE_TIME); // 由于开门判断前的只考虑电梯目前剩余容量,在乘客下电梯后电梯目前剩余容量更新,策略类需要再次调用remove函数 if (strategy.updatePersonToPick(this, requestTable)) { personIn(); } output.printMsg(String.format("CLOSE-%c-%d-%d", getBuildingName(), this.floor, this.id)); } }
强测结果:99.9015
架构设计
在第二次作业基础上:
- 删除了乘客类的横向/纵向移动乘客类,为乘客配置了路线规划/设置当前起点/设置下一步终点/判断是否可上某个电梯的方法
- 增加两个全局的 Channel -
PersonMsg
(管理乘客信息,所有乘客请求完成+输入关闭后结束所有电梯进程) 和ElevatorMsg
(管理电梯信息,方便乘客规划路线) - 增加电梯楼座掩码类(判断电梯可达性)
- remove操作改为由策略类进行(防止电梯开门但接不到人)
类图如下:
UML协作图
BUG分析
评测机构建
测试程序:黑盒测试,使用python的subprocess块运行得到输出
正确性判断:静态分析输入和输出,模拟电梯和乘客行为
电梯行为正确性检查类图如下:
个人bug分析
三次作业在强测/互测中均未出现bug。
第二次作业提交前本地测试中出现的bug有电梯到达未知楼层(0层),原因是LOOK算法的转向移动判断未考虑电梯所处楼层,解决方法为加入边界判断。
第三次作业提交前本地测试中出现的bug有极度消耗CPU时间的操作(能提高电梯性能,但普通70条指令本地测试CPU时间为5s,故舍弃了)、忘记考虑横向电梯可达性(竟然过中测了)
他人bug分析
主要通过自己搭建的评测机+构造特定数据进行HACK
第一次作业
共hack出2个bug,①使用线程不安全的集合时未采用互斥操作 ②未封装线程安全的输出
第二次作业
共hack出1个bug,使用线程不安全的集合时未采用互斥操作
第三次作业
共hack出1个bug,错误规划了横向移动乘客的路线,认为同一楼层可以直接乘坐横向电梯直达
心得体会
多线程设计感受
相比单线程问题,多线程问题的起步无疑给我带来了很大挑战。一方面,开始很难想象因多线程运行次序的不同导致不同的结果,另一方面多线程的测试也比较困难。个人认为在设计多线程程序时,脑海中一定要隐隐地有这几个线程的协作图,能意识到哪里会可能出问题,哪个方法需要加锁,哪里可能出现死锁等等。
要保证线程安全,个人认为最主要有2点:
- 采用成熟的多线程设计模式。生产者-消费者模式作为比较简单的多线程模式,大大降低了本次作业的难度。
- 多做测试。例如模拟高并发时的情景。
其他感受
电梯月最大的感受还是多做测试。毕竟一周时间是有限的,个人每次作业大概有至少1/3的时间花在评测机的迭代上(也是吸收了第一单元专注于优化忽视正确性的教训),就算时间不足以完成自己目标的优化,也要保证自己程序的鲁棒性。此外,本次HACK出的所有bug都是稍微做一下测试就能测出来的,也可见自己写评测机的重要性。
其次是设计模式的使用。本次作业主要采用了单例模式、策略模式和工厂模式,大大降低了类与类之间的耦合,保证了程序良好的可扩展性。(设计模式真的是展现了程序设计的美感)
最后是局部优化和全局优化的关系,局部优化并不意味着全局优化。第三次作业有一些同学使用了最短路来规划乘客路线,以求得局部的最优解,但实际表现却并不好(因为大量的换乘/开关门快速消耗了最短路优化带来的效益)。反而较简单的、不考虑横向电梯的换乘的策略在本次作业中普遍表现较好。同时,与其考虑策略带来的效益,不如试着提高电梯在不考虑策略模式的性能,如避免自由竞争中开门接不到人的问题(个人第二次作业出现的问题)。
总而言之,OO是一门需要创造力的课程,从中能获得搭积木的乐趣:)