BUAA_OO 第二单元总结——电梯
前言
第二单元的主要内容是多线程电梯的搭建,难度不大,关键在于对于多线程的理解以及对锁synchronize的使用上要多加注意。加锁这块还是很有意思的,我在第三次作业加锁那部分卡了很久,才明白了锁怎么加的。
第二次作业的代码量以及难度无疑小了很多,这单元的第一次作业(164行)写完了之后回顾上一的第一次作业(1000多行),无疑留下了心酸的泪水。由于前两次作业检查的比较仔细,第三次作业测试上就有点小摆,然后就翻车了。
第一次作业
简介:模拟电梯的运行,每座竖直方向上只有一台电梯。
- 代码量:156行(改后)
- 大小:5KB
- 强测:99.4294
神中神!第一次作业的代码量竟然如此少。至于为什么是修改后,因为我提交之后才发现很多地方可以再简化一下,就这么短了。原来是202行,化简的地方并不是算法,而是代码。
本来打算第二次作业分享的,因为我看往年的第二次作业好像就是自由竞争了一下,后来发现好像不是这样,然后只能直接说了。
架构分析
每个电梯专门配一个请求池(其实就是ArrayList
,我本来想用stream的,结果发现太慢了),电梯直接从请求池里面获得输入。
电梯每一次循环运行步骤为:1.出2.进3.关门。(开门在进出之间)
对于纸片人的话,我是直接不合理设定,直接开门0.4s。
我本来想用一个调度器的,输入请求到调度器中,调度器为每个线程池分配请求,结果我发现好像用不到调度器,直接输入到请求池中就行。
算法:LOOK算法
我看好像直接搜LOOK算法只有一个方法介绍,每个人到具体落实上好像不太一样,比如我室友之前就是往上走的时候什么人都接,最后时间就有点长。简单说一下我的:
每经过一楼判断一次请求:
电梯没人,如果运行同方向有请求则向同方向走,反方向就改变方向反方向走,否则等待。
电梯有人,继续往同方向走。
每到一层只接同方向走的人。
一些接人方法:
- 先出后进
- 先判断能否进出再选择开门
优化算法:
- 上下行量子电梯:电梯在没请求的时候不懂,有请求的时候根据已经等待时间选择上下行多少时间
我没写的优化算法:
-
请求池的请求最开始就分为上下行两组
-
进出量子电梯:在等待的时候直接让0.4秒内进出人员,如果刚关了0.4秒内(上下行量子电梯)还有请求则重新打开电梯
和上下行量子电梯综合使用:本来是0.4秒上下行,现在电梯不懂,到了0.4秒的时候再决定上下行。
结构评价:
神中神,已经找不到比这更短的了。
结构优化——lambda表达式
我发现lambda表达式真的是一个好用的方法。(之前不会用,我太菜了
电梯进入:
ArrayList<PersonRequest> requests = new ArrayList<>(waitPool);
requests.removeIf(request -> {
boolean requestStatus = request.getToFloor() > request.getFromFloor();
return !(request.getFromFloor() == eleFloor && status == requestStatus);
});
List<PersonRequest> list = requests.subList(0,Math.min(requests.size(), 6 - capacity));
waitPool.removeIf(new ArrayList<>(list)::contains);
这里我将请求池的内容全部复制下来,然后除去不满足条件的,再用subList
根据载客量选择请求数量。
ArrayList
的removeIF
、forEach
,stream的anyMatch
都可以用lambda表达式,极大地节省了代码。
其次,我发现好像主线程也不需要啊,直接把输入线程挪到主线程就好了(乐)
所以最后的代码减少了。
调度器和锁
调度器:没有使用调度器,输入请求直接到每个电梯的请求池中。
如果硬算的话请求池可以算调度器,只不过不是线程
锁:共享对象只有请求池里面的请求和是否停止运行程序,因此添加请求和删除请求以及设置电梯运行状态都在请求池内完成。
因此加锁方面为对线程池里面的所有操作都加上方法锁,之后的架构也是尽量对请求的操作尽量在一个类(请求池)里面
bug分析
啊草,当初对于线程安全还没有一个准确度概念呢,然后在评论区里面看到了说输出线程不安全还没有一个准确的概念呢,然后输出没加锁,直接被hack麻了(乐
第二个问题是对于请求池的请求没加锁,也是对于线程不安全的问题没注意。
其实也有一种不加锁的方法,那就是写请求池请求遍历的时候别用forEach方法,直接用i循环使用get(i)这样就不会因为请求队列即往里面加元素又遍历产生错误了。但是这种方法不推荐。
第二次作业
简介:模拟电梯的运行,增加了横向电梯,行人只能横着走或者竖着走,可以动态增加电梯。
本质上和第一单元没区别
- 代码量:324行
- 大小:12KB
- 强测:97.9869
架构分析
横向电梯和竖向电梯做的工作并没有本质区别,只是里面方法的内容有一些区别,因此完全可以增加两个横向电梯类和请求池,具体方法和竖直电梯一样,只是条件有些不同。
竖向多电梯调度算法:LOOK算法 + 自由竞争
这个应该是最快的
横向多电梯调度算法:
-
模仿LOOK算法 + 自由竞争:也就是根据请求路线的远近将分为顺时针和逆时针
-
LOOK算法同楼座都接 + 自由竞争:指不同于我使用的直接同方向的人,这次只要同楼座有请求就接。
时间分析:与第一个没有什么区别(至少在第二次作业的强测上没有区别)
-
ALS算法
时间分析:这个理论上同楼层三个电梯以内比LOOK要快
结构评价:
我这次偷了点小懒,横竖向电梯和请求池理论上可以用同一个类来继承的,但是我并没有,这样更符合面向对象的架构。
调度器和锁
调度器:无,和第一单元一样,没有本质区别
锁:由于增加电梯,且采用的是自由竞争的方式,因此共享对象为请求池里面的请求,因此锁也和第一单元一样。
bug分析
无bug
第三次作业
简介:模拟电梯的运行,行人出发楼层和楼座都不同,电梯信息增加。
- 代码量:490行
- 大小:19 KB
- 强测:83.5207
这次作业由于自己的马虎出了点问题,真的绷不住了。在写测试数据的时候由于我觉得反正第一层有横向电梯,只要观察请求能否可达就行,不同管新的电梯,因此在生成数据的时候就每增加电梯,觉得增加横向电梯也就是让自己的路线变快罢了,也错不了,然后就在横向电梯的处理上马虎了(乐
架构分析
这一次因为行人的路线不能由一个电梯完成,因此需要调度器对行人进行分配(调度器终于又回来了)。
行人请求类重构:这里面对于行人路线的分配我采用的是静态分配的方法,即在请求出现的一开始就根据横向电梯的可达性进行分配路线,因此需要行人请求类重构。
行人类多出来了一个ArrayList<HashMap>
,它是根据横向电梯的可达性来分配路线
例如:从A-5到B-7
ArrayList的三个元素分别为:A-5到A-1、A-1到B-1、B-1到B-7
而其中重写的方法例如获取出发地和获取目的地都为ArrayList的第一个元素的内容。
根据ArrayList<HashMap>
(规划路线)选择把请求放到相应的请求池中,当运行完成后除去第一个元素,然后调度器。
调度算法:竖------横------竖最短调度算法。
其他的可能的时间优化:
-
动态分配。这个可能可行,但是代码量会比较复杂。
-
当有两个或者多个横向电梯可达且都路程最短的时候,选择电梯运行速度快的
这个优化有一点,但是不多。
调度器和锁
调度器的结构为:
调度器何时停止:这里我采用了count计数的方法,当请求到目的地时count--,当输入停止且count为0时停止电梯的运行。
之前我采用的是通过判断输入是否停止和电梯是否停止这样方法来判别,因此需要给电梯设置上运行状态,比较麻烦。
调度器和电梯线程的交互、锁的设置:
目前使用的方法:
调度器里面保存请求是使用myRequestRaaryList
在输入请求和获取请求方面交互的是请求池
而在电梯返回请求的时候,共享对象为myRequestRaaryList
。如何设置共享对象有两个方法,一个是电梯里面共享调度器类,另一个为专门共享myRequestRaaryList
(这个下一个方法再说)。
第一个的好处是设置锁的时候可以把调度器里面的所有方法设置为方法锁,比较容易设置锁。
之前使用的方法:
这里面使用请求池作为中间元素进行两个线程之间的交互,请求池里面专门共享myRequestRaaryList
这个属性。
好处在于交互情况只有请求池,不易出错。
不方便的地方在于如何设置锁(其实明白了锁怎么工作也比较简单)
我知道第三次作业才知道然后
notifyAll
原来只能唤醒同一类型的锁,不是唤醒所有的。因为之前我一直用的方法锁。
因此调度器和请求池有共享元素,因此加锁元素为myRequestRaaryList
bug分析
讲一下自己的白痴错误。
我在最开始采用了二维数组passaway[][]
保存横向电梯的可达性。例如,增加三楼的横向电梯,AB座可达,那么就设置为passaway[3][A]``passaway[3][B]
为True
,这样出现的问题是,当三楼增加两个电梯分别在AB和CD座可达时,导致三楼ABCD
四个楼座都任意可达,这样问题就太大了。
修复:将passaway[][]
换为横向电梯序列,直接遍历电梯序列选择最短路径。
自测和互测
数据生成
数据生成我采用了随机生成的方法:
def data_generate(length, serial):
ans = []
time_ans = []
for i in range(length):
from_building, to_building = chr(randint(ord('A'), ord('E'))), chr(randint(ord('A'), ord('E')))
from_floor, to_floor = randint(1, 10), randint(1, 10)
while (from_floor == to_floor) & (from_building == to_building):
from_building, to_building = chr(randint(ord('A'), ord('E'))), chr(randint(ord('A'), ord('E')))
from_floor, to_floor = randint(1, 10), randint(1, 10)
time_ans.append(float(format(uniform(1.0, 2.0), '.1f')))
ans.append("{0}-FROM-{1}-{2}-TO-{3}-{4}\n".format(i + 1, from_building, from_floor, to_building, to_floor))
time_ans.sort()
f = open("stdin.txt", "w")
for i in range(length):
f.write('[' + str(time_ans[i]) + ']' + ans[i])
f.close()
shutil.copyfile("stdin.txt", "stdin_serial/stdin" + str(serial) + ".txt")
本来我想对生成的数据按照时间排序的,后来排序总是按照字典顺序而不是数值,所以后来我把时间生成单独拿出来了,真的乐了
测试方法
大量数据生成+随机轰炸(当然可以通过设置楼层、楼座和时间戳让数据生成的更加紧密一点)
调度正确性
这里有几个正则表达式,先简单写一下,然后再说思路:
a = re.compile("ARRIVE-(?P<ele>[ABCDE])-(?P<floor>[-]?\\d*)-(?P<eleID>\\d)")
o = re.compile("OPEN-(?P<ele>[ABCDE])-(?P<floor>[-]?\\d*)-(?P<eleID>\\d)")
c = re.compile("CLOSE-(?P<ele>[ABCDE])-(?P<floor>[-]?\\d*)-(?P<eleID>\\d)")
in = re.compile("IN-(?P<id>\\d*)-(?P<ele>[ABCDE])-(?P<floor>[-]?\\d*)-(?P<eleID>\\d)")
out = re.compile("OUT-(?P<id>\\d*)-(?P<ele>[ABCDE])-(?P<floor>[-]?\\d*)-(?P<eleID>\\d)")
输出数据我是用的是python调用bat脚本(因为subprocess怎么弄都弄不出来)
@echo off
.datacheck1.exe > time.txt
.1.exe | java -jar JAR/%1.jar > out_%1.txt
echo "bat over"
判断电梯调度,我是根据输出来判断电梯的运行情况:
Arrive:
电梯是否在1-10
是否上行或者下行1楼
In:
电梯门是否打开
电梯楼层和行人出发楼层是否相同
电梯是否超载
该乘客是否在等待用户里面
Out:
电梯门是否打开
电梯楼层和行人目的楼层是否相同
该乘客是否在电梯内
该乘客是否在等待用户里面
Close:
电梯门是否打开
Open:
电梯门是否关闭
第一次作业是这样的,然后第二次或者第三次就不用检查那么多了,毕竟前面的过了。
python多线程
threading.Thread
方法能使用多线程,这无疑增加了自测和互测的速度。
但是,bat脚本和os.system
都不能多线程工作,那我们就多生成几个脚本文件,同时运行多个脚本即可。
输入线程不安全:由于开启了多线程,但是脚本文件的输入都是同一个stdin,因此可能出现的问题是输入线程不安全的问题,遇到这种情况,就多测几次这个数据,或者单线程多测几遍。当然,也有可能是自己本身的线程不安全导致的。(我记得我有一个数据测170多次才会出现一个线程不安全,我当初找了半天哪错了,结果还是没问题的)
UML图
UML类图
UML协作图
注:本图只画了各个类之间方法的交互,并没有画构造方法的交互。同时,由于横竖向电梯和请求池相同类型,因此只画了一个。
分析和总结
扩展上:易于扩展,不同电梯有不同的内容和请求池,在增加类似电梯的时候易于分离。
不足:时间性能上,没有用上述的优化方法使时间最佳。结构上,请求池和电梯类都能统一起来,而自己并未取二者的共性,层次化架构上并不美观。
心得体会
这单元的作业相较于上一单元无疑是稍微轻松一些的,但线程这一部分真的值得再仔细学习扩展。
线程安全:在本单元的学习过程中,我采用了大量方法锁的方法,因此线程较为安全。但是在第三次作业中,当我真正的使用了类锁的时候,却已经忘了它的涵义和使用,经过学习之后我已经对synchronize锁有了较为深刻的了解。
层次化设计:学会抓住事物的本质,当我在第三次作业规划时,如何将调度器和电梯与输入线程三者之间分离,同时又能传递信息是一个较大的挑战。