I 对问题的初体验
在开始OO之旅前,对OO电梯早有耳闻。这一次终于轮到我自己实现OO电梯了。首先从顶层需求出发对电梯系统进行分析,对象包括电梯、任务和乘客。对于乘客而言,因为一个乘客由ID标识且仅会在一个生命周期中产生一个请求,因而可以和任务合并一体,作为一个输入线程实现。经过上述简化电梯调度模拟系统最核心的部分就落在了电梯模块和任务调度模块上。在每一次电梯作业的更迭里,慢慢寻找工程化和优化之间的微妙平衡。
II 三次的设计思路
A 单电梯 FCFS 调度算法实现
可以说这个作业算是电梯系统的开始。其本意可能是让我们分析出这个问题的对象并且实现基本的线程思想。在这个任务中,我将主函数线程和输入轮询线程合并,赋予其初始化与轮询获取输入的功能。对与调度器我认为没有必要使其成为一个独立的线程,而应该让他成为一个共享对象在各个电梯之间共享。这一次的目的选层式的电梯设计,输入输出接口的简化以及连续的正数楼层给了我们充分的思考和准备时间,让我们更合理的设计电梯。在线程的安全性方面,电梯需要访问调度器中的任务队列完成任务的分配,且任务队列还需要接受输入线程的输入请求,因而在每一次操作时都应加锁。
B 单电梯多楼层捎带调度算法实现
这一次的作业相较上一次,增加了调度算法的复杂度,也增加了地下楼层这一设定。在最开始就要牢记 0层不能停 的事实。对于捎带的实现,我使用一个任务队列存储所有当前上线的任务,并且定义了电梯内部正在执行的计划类。对于电梯内部的计划,包含电梯当前的运行方向,需要停靠的楼层,以及在每一个楼层上下电梯的乘客号。在电梯到达或经过每层时,会向调度器请求捎带任务。调度器负责过滤出可捎带任务,之后加入电梯计划中执行。在测试中发现很多时候因为评测样例喜欢在0秒钟塞入成吨的数据,使得没有来得及读入的请求不能被很好的捎带。多线程间的协同体现在输入模块读取输入,电梯线程获取相关计划,和第一次作业类似,只需要对任务队列加速保护即可。
B+ 单电梯多楼层捎带调度算法优化
在优化中,我选择在每个任务到达的时候,调度器会首先将未分配的任务按照一定的规则组合成电梯计划,当电器请求时一并交给电梯执行,这样可以保证等待队列的顺序是贪心的最优解,提高算法的效率。但是在实际强测过程中因为时间间隔较短、评测用例较为规律化导致这种算法的效率不算很高,甚至有时会弱于扫描算法。
调度器组合请求的顺序根据一个性能函数来判断贪心最优解:对于每一个电梯计划的可插入位置,计算该电梯计划因为新添加的计划所导致的额外的开销。若某一处加入后的开销最小,且小于任务本身的开销时间,则选择在该位置插入任务。否则,将任务单独作为新的电梯任务插入。
同时,在电梯运行过程中,也会不断查询调度队列寻找可以加入的新计划。新计划需满足:和当前电梯运行方向相同、电梯尚未到达起点楼层且计划间有楼层重叠。
当电梯执行好一个计划后,优先选择调度器队列中距离最长的反向任务执行。(受电梯扫描算法启发)当当前任务执行完毕时,电梯可以偷窥下一个对应的任务的起始楼层是否和当前电梯所在楼层相同,若相同则可以省去一次开门的时间。
C 多电梯多楼层捎带调度算法实现
第三次作业从体量和内容上都比第二次作业增加了不少。其中还最大的不同还属于电梯能够停靠的层数发生变化,且一个请求可能需要多个电梯之间的协作完成。对于这个问题,为了提供一个统一的解决方案,我决定使用一张图来描述整个电梯系统的状况。图中的节点为电梯系统所有可以到达的楼层,楼层间的边则代表可以在两层间运行的电梯。对于一个请求,只需要在图中计算最短路即可得到拆分后的任务队列。
在前两次作业的基础上,电梯类可以说完全沿用了第一次作业的设计。为了适配多电梯协作任务的完成,为计划队列增加待完成计划这个属性。从设计上来讲,我希望在调度队列中的所有任务均是待命状态,这就需要协同任务的后续请求需要在前序任务完成时出现在队列中。这样的设计可以极大地简化调度队列的维护和查询,提高代码简洁度。多线程之间的协同产生于输入线程为调度器提供输入,电梯向调度器请求任务执行。为了保证线程安全性,需要确保共享的调度器中的关键对象——调度队列在读写过程中加锁。
C+ 多电梯多楼层捎带调度算法优化
在完成基础图算法的基础上,开始探寻优化的空间。对于图算法,边权重的设计就值得考虑了。在优化版本中,我考虑为图的边赋予一定意义的数值。具体而言,对于可以直达的边,其时间开销为一次开门时间附加该电梯在两层之间的运行时间。对于不可达的边,其权值为中介可达路径的时间开销总和。此外,还需要额外附加电梯当前位置到任务起始位置的响应时间,以确保局部的贪心算法。这样,在图中运行 Floyd-Warshall 算法获得任意两点间权值最小的路径,即是在当前时刻最优的分配。
值得注意的是,图算法仅能够提供当前多个电梯协同任务的第一段分配。其他分配过程需要根据该任务完成时的电梯状况而定,不应该提前划分。这种优化方式也带来了一些潜在的问题。其中之一就是,不同的任务在不同的时间点可能被分配给不同的电梯来执行,这就要求当电梯在空闲状态是需要以一定的时间间隔检查是否有可以执行的任务来执行,而不能用通知的方式来实现。但是鉴于电梯运行时间较长,所以间隔查询的时间也不需要很长,所以这个过程并不过分消耗CPU时间。
Bug
明明知道 LinkedList 线程不安全但是还是鬼使神差的在程序里用了,可能是哪天脑子抽风了写进去的吧...哭晕,又一次错惨了。
III 解决方案的评估
A 自动化测试
这一次,鉴于不同作业要求的电梯输出和功能都略有差别,因而选择搭建一个较为灵活可变的框架实现三次电梯作业的自动化测试。多线程问题错误的出现不可复现,不便于调试,因此选择随机生成测试集,利用测试系统的形式是使用终端脚本运行多个协同的程序并最后检查结果。自动化测试的文件结构如下:
. ├── README.md ├── start.sh └── test_elevator ├── clean.sh ├── comm.py ├── elevator-input-hw3-1.4-jar-with-dependencies.jar ├── elevator_tester.jar ├── gen.py ├── test.sh └── timable-output-1.0-raw-jar-with-dependencies.jar
自动化测试由命令 bash start.sh 开始,执行目录 ./test_elevator/test.sh 脚本。该脚本负责运行主要的 Java-Shell 交互程序 comm.py,由 gen.py 生成随机数量、随机间隔的请求数据并由 Python 作为桥梁输入给待测试的 Java 电梯程序,捕获输出并交给 elevator_tester.jar 检查结果,最终将运行结果返回给 test.sh 脚本。
为了方便不同参数下的自动测试,start.sh 被设计成可以将一些参数写入文件中作为 cache 的特性。在第一次指定必要参数后,之后的运行不必重复进行。
1 #!/bin/bash 2 if [ ! -d "test_elevator" ]; then 3 echo "Dependency Directory test_elevator Not Found!" 4 exit 1 5 fi 6 if [ $# -gt 0 ]; then 7 echo "Setting Cached Parameters: Test_Rounds Max_Requests Max_Interval Java_Main_Path Java_Package_Name" 8 9 echo "$1" > test_elevator/num.cache 10 if [ $# -gt 1 ]; then 11 echo "$2" > test_elevator/request.cache 12 fi 13 if [ $# -gt 2 ]; then 14 echo "$3" > test_elevator/interval.cache 15 fi 16 if [ $# -gt 3 ]; then 17 echo "$4" > test_elevator/project.cache 18 fi 19 if [ $# -gt 4 ]; then 20 echo "$5" > test_elevator/package.cache 21 fi 22 23 uname > test_elevator/system.cache 24 else 25 if [ ! -f "test_elevator/num.cache" ]; then 26 echo "Parameters Test_Rounds Unset!" 27 echo "Try Setting Parameters By:" 28 echo -e "\t./start.sh Test_Rounds Max_Requests Max_Interval Java_Main_Path Java_Package_Name" 29 exit 1 30 fi 31 if [ ! -f "test_elevator/request.cache" ]; then 32 echo "Parameters Max_Requests Unset!" 33 echo "Try Setting Parameters By:" 34 echo -e "\t./start.sh Test_Rounds Max_Requests Max_Interval Java_Main_Path Java_Package_Name" 35 exit 1 36 fi 37 if [ ! -f "test_elevator/interval.cache" ]; then 38 echo "Parameters Max_Interval Unset!" 39 echo "Try Setting Parameters By:" 40 echo -e "\t./start.sh Test_Rounds Max_Requests Max_Interval Java_Main_Path Java_Package_Name" 41 exit 1 42 fi 43 if [ ! -f "test_elevator/project.cache" ]; then 44 echo "Parameters Java_Main_Path Unset!" 45 echo "Try Setting Parameters By:" 46 echo -e "\t./start.sh Test_Rounds Max_Requests Max_Interval Java_Main_Path Java_Package_Name" 47 exit 1 48 fi 49 if [ ! -f "test_elevator/project.cache" ]; then 50 echo "Parameters Java_Package_Name Unset!" 51 echo "Try Setting Parameters By:" 52 echo -e "\t./start.sh Test_Rounds Max_Requests Max_Interval Java_Main_Path Java_Package_Name" 53 exit 1 54 fi 55 echo "Starting Elevator Autotest..." 56 cd test_elevator 57 num=`cat num.cache` 58 request=`cat request.cache` 59 interval=`cat interval.cache` 60 project=`cat project.cache` 61 package=`cat package.cache` 62 echo -e "Current Parameters:\n\tRounds :\t${num}\n\tRequests :\t${request}\n\tInterval :\t${interval}s\n\tMain : \t\"${project}\"\n\tPackage :\t${package}" 63 ./test.sh 64 fi