多线程电梯设计的总结与反思
Posted thunderzh-buaa
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了多线程电梯设计的总结与反思相关的知识,希望对你有一定的参考价值。
一、FAFS电梯设计
1、共享资源的数据同步
2、整体架构
先考虑第一个问题:
数据同步的问题显然可以使用synchronized解决,也就是经典的生产者消费者模型。
但是由于初次接触,对锁机制理解不清,我还探索了一种不那么好的方法——volatile,具体使用方法见Whai is volatile
它显然能用在这个问题的解决,但是其实volatile带来的问题很多,比如性能损失,也不具很好的普适性。
第二个问题:
FAFS在调度策略的实现是非常简单的,因此更多思考留给了如何设计一个良好的架构使得其策略易调整,电梯易拓展等问题。
我在这次的设计中,更多得考虑到了策略的可延伸性。
我认为所有的调度无非就归结到两个方面,每个楼层有无请求,电梯up or down。
所有的请求一定是人在确定的一层上发出的,因此与其考虑维护所有请求队列,不如维护各个楼层请求。这给调度时候的扫描判断带来一定的便利。(后来我发现这样做有个问题:如果只维护楼层请求,只针对当前楼层调度,会使调度策略具有局限性,可能全局考虑才可以做出更好的优化)
3、两种实现方式的对比
1、volatile
(2)Synchronized
相同点:
Ask类是自己封装的一个请求queue类,主要是解决java自带Queue类的功能不能完全满足我的设计需求的问题,这里设置的flag是为了exit而特意准备的。
Elevator是电梯类,设计缺陷在于未考虑继承,调度策略全盘体现在goTo方法中。应该考虑针对openDoor(),closeDoor(),printOut()等电梯基本操作作为一个父类,针对后续不同的电梯进行继承并补充。第二次作业也考虑到了这个问题。
InputHandler是做输入处理,是三次作业几乎一直沿用的一个类
不同点:
Elevator 和 inputHandler共享volatile修饰的askList,两个类都可以操作同步的askList。
Elevator看作producer,inputHandler看作consumer,利用一个Tray类做线程的数据同步。其中使用wait/notifyAll来解决consumer反复轮询造成的性能损失,是有利的。
二、AlsElevator设计
这大概是真正java多线程的入门,第一次作业一直在探索阶段,一直在查阅相关的资料,进行了学习传送门。
这一次算是正式引入了我自己楼层请求的想法和生产者消费者模型。
先来谈一谈楼层请求的思路:
Floor给每个楼层设置了askIn,askOut,askQueue队列,分别储存该层要求进入的请求,要求出去的请求,所有请求。
input后只需把相应请求put进对应楼层以及总请求队列
public void put(PersonRequest request) {
...
askQueue.add(request); //总请求队列
askIn[floorTrans(request.getFromFloor())].add(request); //from floor 的 in 队列
}
每次到达一个楼层,只需遍历该楼层的in、out队列即可(只谈Als,不考虑其他更好的策略)。并且in之后,需要把相应请求给到目的楼层的out队列,实现的大体框架如下
loop:
arrival();
checkInOut();
FloorAdd();
?
private boolean checkIn() {
...
long lastDate = new Date().getTime();
while (abs(new Date().getTime() - lastDate) < 400) {
if(ifTake()) { //捎带判断
...
askQueue.remove(); //移除
askOut.add(); //相应楼层增加out请求
...
}
}
}
?
private boolean checkOut() {
...
// scan askOut
printOut();
askOut.remove(); //相应楼层移除out请求
...
}
?
private int checkInOut() {
checkOut();
checkIn();
}
如此之下,我们只需(1)设置好mainRequest (2)不断update loop的最极限楼层 (3)捎带条件判断
之后,一个完整的Als电梯就大功告成了。
个人认为这种楼层请求思路对Als的实现还是相当友好的(但可能仅限于Als)。
结构安排
Elevator类实现电梯各种最基本操作,开关门,到达...
AlsElevator类继承基本的电梯类并实现了Als调度算法
inputHandler处理输入请求。
Tray作为托盘,协调AlsElevator和inputHandler,进行put,get操作。(生产者消费者模型)
Floor包含所有楼层的in、out请求,放置在托盘中,供输入线程和电梯线程实时操作。
复杂度分析
复杂度问题主要在于AlsElevator电梯,查看方法复杂度发现:
问题在于
getIn方法:
openDoor()分散在方法中,导致反复判断,应该额外操作
反复遍历tray中Floor存的队列
电梯能去的极限楼层的判断没有独立出来。
goTo方法:
楼层的循环采用for并且反复修改fromFloor 和 toFloor
ifTake方法:
过多过复杂的布尔表达式
解决方案:
取消for循环控制,直接针对各个楼层判断电梯up or down,放置于run,降低判断语句使用频率。
open和in 、out分离,checkIn checkOut只针对是否出入,开关门新设方法控制。
每层设置一个方法判断电梯极限楼层。
布尔表达式化简。
修改调度方案,无需过于依赖mainRequest,让调度接近scan算法,不仅能有效减少mainRequest带来的比较多的判断,简化布尔表达式,还能提升性能。
三、SS电梯设计
真*重构huozangchang的实例
这次使用了Worker Thread设计模式
这次的设计并不好,所以主要做一个总结和反思
经过思考,本次作业我没有继续沿用之前的楼层请求思路(其实是可以的),由于存在拆请求调度等问题,这个思路操作起来相对困难,因此我使用了一种相对比较好实现的思路。
实现思路
(1)把请求分成两类,可乘坐电梯直达的和需要一次换乘的。设置request类,用于装入请求,并在装入时判断该请求是否可直达,如果可直达,必须乘坐直达电梯,如果不可直达,在请求被电梯读取时,选取该电梯能到的最近的换乘点。所有的请求的读取由多线程电梯自己竞争。
(2)设置调度器分派请求,其中有两个队列inputQueue和requestQueue,其中inputQueeu用于与inputHandle交互,requestQueue与电梯交互,由于电梯运行过程会操作并锁住请求队列,为了防止此时读入被拒绝,因此再设置一个队列。只需在调度器中更新requestQueue即可。
(3)电梯类只需实现Als电梯的基本功能,再重写floorAdd()保证不该停的楼层不停,以及重写checkOut()针对换乘请求放置新的请求给requestQueue即可。实现较为简单。
线程安全
线程安全的保证可能是本次设计最大的难点。
线程不安全主要出现在对inputQueue和requestQueue两个队列的操作处。
Scheduler中:
(1)put()方法:将请求放置于inputQueue,锁住inputQueue,保证数据同步。
(2)upgrading()方法:锁住inputQueue和requestQueue,requestQueue新增请求,inputQueue删除请求。
(3)get()方法:用于三部电梯得到请求,锁住requestQueue,需要删除requestQueue中的请求。
MoreElevator中:
(1)由于Als捎带规则,每次checkIn()如果有人进入需要访问requestQueue并删除请求,锁住。
(2)由于换乘规则,每次checkOut()如果某请求到达换乘点需要新增requestQueue中的请求,并删除原请求,需要锁住requestQueue。
至此,所有线程不安全的问题已经通过synchronized解决了。
复杂度分析
从上述数据可以发现,
MoreElevator和Request两个类具有比较高的复杂度。查看相应的方法可以看出
问题在于:
MoreElevator中:
getIn(),ifTake(),goTo()存在几乎与第二次作业相同的问题(因为实现几乎是沿用的)
Request中:
adjustDirection()用于判断该电梯中的最近换乘点,由于对A,B,C三种电梯集中判断,过多的if语句和过复杂的布尔表达式使这个方法复杂度爆掉。
getStop()用于服务adjustDirection(),寻找该请求所有换乘站。也对A,B,C三种电梯集中判断,过于复杂。
解决方法:
换乘点的判断和最近换乘点的寻找在装入相应的电梯时再判断,避免冗余。
四、bug分析
三次作业只有第三次作业在公测中被发现bug,互测中第三次作业被hack20次(太致命了)
这些bug都有一个相同的报错RUNTIME ERROR
最后我发现所有测评的cpu时间过长,自己反复分析无果之后,求助插件,并探索了一套方法。
希望和大家分享、交流,给广大cpu时间问题的朋友们一个帮助。
CPU time分析与解决
以第三次作业为例
本次出现了较多的cpu时间超时的问题,大量的问题也让我自己摸索了一条定位,分析和解决cpu time问题的方法。我主要使用VisualVM插件进行分析。
1、运行VisualVM,使用抽样器进行CPU抽样
2、在不输入任何请求的情况下观察线程cpu时间,发现main线程在无请求过程中占用大量的cpu
因此决定解决main的cpu时间问题。
切换到查看方法的cpu,发现upgrading方法占用大量cpu
于是发现upgrading方法在inputQueue为空时持续工作,存在暴力轮询,考虑使用wait/notify进行修改
public void upgrading() {
synchronized (inputQueue) {
while(inputQueue.size() == 0) {
inputQueue.wait();
}
synchronized (requestQueue) {
...
}
inputQueue.notifyAll();
...
}
再做一次抽样,发现
main线程cpu时间显著下降。upgrading方法几乎无cpu时间。
因此,无输入等待过程cpu时间问题基本解决。
3、输入足够多的请求,保证足够长的观察时间
运行过程电梯线程是占用cpu时间的主力,并且,相比无输入过程总cpu时间显著上升,速度较快,也就是说,一旦输入请求过多,电梯线程对cpu时间的占用必然导致最后超时,因此定位到了电梯线程的问题。
接下来,查看各方法:
检查自己的设计发现get方法已经是由wait/notify,goTo,floorAdd是普通的循环逻辑不存在暴力轮询。
接下来,重点就是getIn,checkIn了,观察数遍后发现,笔者上一次作业遗留下来一个大坑:
private int checkIn (...) {
...
long lastDate = new Date().getTime();
while (abs(new Date().getTime() - lastDate) < 400) {
if ((temp = getIn(hasOpen, from, maxFloor)) != 0) {
....
}
...
暴力轮询显而易见。
因此,做了修改
private int checkIn (...) {
...
long lastDate = new Date().getTime();
while (abs(new Date().getTime() - lastDate) < 400) {
if ((temp = getIn(hasOpen, from, maxFloor)) != 0) {
....
}
sleep(long(399.9)) //防止暴力轮询,先睡一会再起来检查吧
...
重新CPU抽样
总CPU时间增加缓慢,电梯线程cpu时间下降,OK了!!!!
到这里,笔者的CPU time基本控制在一个慢增长,并且稳定在10s以内,问题已经解决。
五、发现别人bug策略
主要采用了python写的对拍器和Python写的随机样例生成以及python书写的简单输出结果检查进行测试。
只在第三次作业发现别人的bug,主要集中在:
1、程序退出错误,提前退出问题,需要大量对拍
2、程序退出错误,程序无法退出
3、输出逻辑问题,某些楼层未关门
4、换乘问题,未成功换乘就停止了
5、cpu_time_error,部分程序存在较为严重的暴力轮询问题。
1,2,3,4主要通过对拍器发现。
5 主要通过VisualVM运行对应的程序发现。
本次测试与之前最大的差异在于需要定点投放,我自己写的python程序实现不了,后来通过讨论区大佬的方法实现了定点投放。
同时本次增加了cpu_time问题的测试,我借助了手动运行VisualVM的方法。
六、总结
线程安全的分析主要关注所有对共享资源的访问上,在操作共享资源的方法,代码块,要考虑是否加锁的问题。一定要对所有共享资源的访问做覆盖性的分析。
多线程设计要关注暴力轮询的问题,多用wait/notify,尽量减小CPU时间。
擅用插件分析cpu、内存等情况。
七、心得体会
以上是关于多线程电梯设计的总结与反思的主要内容,如果未能解决你的问题,请参考以下文章