BUAA_OO第二单元多线程电梯总结
第一次作业
一、同步块及锁
首先,在读入等候队列的时候,我将调度器用synchronized锁住了,因为电梯和输入线程都会对调度器的电梯等候序列做出影响。
while (true) {
PersonRequest request = elevatorInput.nextPersonRequest();
if (request == null) {
synchronized (scheduler) {
scheduler.setClose();
scheduler.notifyAll();
}
break;
} else {
synchronized (scheduler) {
scheduler.setPersonRequests(request);
scheduler.notifyAll();
}
}
}
后来我发现这样还不够,还需要处理CPU轮询的问题,但总是没能取得很好的效果,最后发现是唤醒的时候没有唤醒电梯带的锁,是我自己对唤醒机制理解错误。最后代码如下:
public void run() {
while (!needWait() || !scheduler.isEnd()) {
synchronized (this) {
while (needWait()) {
if (scheduler.isEnd()) {
return;
}
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
action();
}
先锁住电梯,当判断需要等待的时候进行等待。当scheduler判断有输入或者输入结束的时候由调度器唤醒电梯。
二、调度器分析
第一次作业为单部电梯的操作,故不涉及多电梯之间的算法,只有针对一个电梯的内部调度算法。
经过查找各方面资料,我最后锁定了look算法。
ALS算法:主要是聚焦于电梯内乘客的请求,分主请求和捎带请求,以主请求目标为电梯主要目标。若电梯内无乘客,则去接最早等待的顾客。
look算法:聚焦于乘客对于各楼层的请求,若该电梯为上行状态,则去接所有上行状态的乘客,直到把这一次上行可以接到的乘客送到需求的最高层。然后去查找更高层是否有乘客有请求,若有,则继续上行,直到满足所有乘客请求中最高层的请求。向下的情况反之。
look算法的情况相较而言更加简单而且速度更快,对于morning和night,night模式没有任何区别,morning的主要区别也只在于刚开始在到来6人以内时,若有人来就多等2秒,直到2秒内无人来或已经有六个人。
我设置了floor类的数组容器用来将获取的乘客请求放置于各个楼层,并让每个电梯内部放置一个该容器,用于帮助电梯判断乘客对于各楼层的请求。电梯线程循环运行过程中,先设置目标楼层,然后判断当前层是否可以开门,若开门则先下后上,然后向目标楼层移动。
这次我将单部电梯相关的调度全部放在电梯内部,调度器仅负责多电梯相关的调度。电梯内部调度思路如下
public void action() {
this.floors = scheduler.getFloors();//更新楼层信息
if (needOpen()) {
open();
//passenger in and out
close();
}
setDestiny();
if (destiny > floor) {
goUP();
} else if (destiny < floor) {
goDown();
}
}
调度器负责从从输入队列读取请求,传给各个楼层,并把楼层请求信息告诉给电梯。当电梯的楼层和电梯内部没有请求时会先等待,待又有请求,由调度器将他们唤醒(避免轮询)。当没有输入时,调度器将告诉电梯输入停止,这时等待中的电梯可以结束进程。
三、bug分析
第一次作业本该是bug最少的作业,但在满员的情况中判断出现了错误,并且与判断是否该开门的算法产生了冲突,结果导致电梯会停留在一层不断开关门。有许多点因为这一个bug而挂掉了。
第二次作业
一、同步块及锁
第二次作业与第一次作业的区别在于涉及了多个电梯的调度情况,我让多个电梯竞争一个等待序列,并未涉及调度器,所以只需要考虑多个电梯争抢一个队列时可能出现的线程不安全的情况。
为了避免这种情况,所以应该在电梯对于某一楼层行动时锁住楼层,避免多个电梯同时对某一楼层操作,多上下人的情况。
且对于每个电梯的wait(),为了唤醒每个电梯,用for循环遍历了电梯,并逐个唤醒。
二、调度器分析
第二次作业中,我在同类电梯之间没有进行相关的调度,就是让他们共享一个请求队列,然后进行竞争。
在做第三次作业之后,我意识到可以采取相似的办法做第二次作业,即每个电梯设置一个等待请求队列,由调度器将乘客分配到各个请求队列中去(可以根据当前各电梯请求队列的人数进行平均分配),这样可以避免多电梯争抢但最后某电梯竹篮打水一场空,白白浪费性能的局面。
三、bug分析
第二次的正确性方面没有bug,但是有一个bug会导致性能上的问题,所以第二次的强测强测因为性能问题挂掉了两个点。这时作业的主要问题是由于电梯线程和输入线程没有做好,所以电梯线程启动的时候,输入线程往往只能读入几个点,就被电梯线程抢了先,而且电梯线程运行完,输入队列才会继续读入。导致消费者线程每次只能处理3、4个请求,性能很差。另外,电梯的竞争调度也会导致好多电梯白跑一趟。其实完全可以像第三次给不同电梯设置队列一样,给同类型的电梯也设置自己的队列,优化性能。
第三次作业
一、同步块及锁
第三次作业与第二次相比,差距更小,只是设计了三种不同的电梯且分派了三种等待队列,而我的调度器并非作为线程而作为一个类存在,读入队列时把调度器上了锁,电梯操作时锁住了楼层。线程安全问题甚至比第二次作业还小,故没有设置其他的锁。
二、调度器分析
对于第三次作业中不同类的电梯,我也没有设置换乘相关的调度,只有很简单的分配。一开始还以为是跟现实电梯相差无几的低层梯、高层梯的换乘策略,后来发现并不是那么回事orz。
把原来的floor的数组复制三份,不同类型的电梯共享该类型的请求队列。对于请求队列的分配,我给能用c类型(即速度最快、但条件最苛刻的)运送的乘客全部分配到c的队列中去,其次分配b类型,最后再分配a类型。虽然分配策略十分简单粗暴,且效率比较差劲,但不失为一种可以ak的策略。:)
对于每个请求队列,由于不能到达的楼层的请求一定为0,故不需要进行多余的设置。
三、bug分析
第三次强测所有点都过了。
互测中没有hack到其他人的bug。
四、架构设计
由于第三次作业是在第一次、第二次作业的基础上扩展出来的,且基本架构没有太大区别,故只分析第三次作业。
UML类图
共有五个类,分为主线程、电梯线程、输入线程、电梯类和调度器类。
这五个类自单线程电梯调度以来就有,所以可以说第一次作业的可拓展性比较强,省去了重构的时间,二、三次作业的改动也非常少。第二次增加了多个电梯,第三次增加了不同电梯的分别,并设置了不同类型电梯的调度队列。但是由于第三次的分配方法是已知三种电梯各自的信息而设计的,若是改变各种电梯的楼层等信息,或者增加可以自定义楼层的情况,就需要更多的改动了。
主线程:主要负责创建其他线程,并无多余作用。
等待队列:负责读入队列信息并传入给调度器,在生产者——消费者模型中为生产者。
调度器:负责接受请求,并依据请求创建电梯或者在楼层中加入乘客请求,根据不同类型电梯分配各乘客请求,以实现调度多电梯的目的。在接收完请求之后,给电梯结束的命令。
Floor:由调度器创建,可被电梯类读取。实际使用时应该是包含Floor类的数组,一个Floor代表一层,可判断是否为空,是否有上、下的请求,控制楼层中乘客的出去。为模型中的托盘。
电梯类:一开始默认电梯由Main类创建,剩下通过加电梯指令添加的电梯由Scheduler创建。自身内部有20层的floor数组和电梯内乘客队列,为消费者,由自身内部调度器判断应该怎样接、送乘客。
可以看出由于把电梯的内部控制全部放入电梯中,电梯类的内部情况比较臃肿,方法复杂度较高,其中最复杂的是是否需要开门和设置目标的两个方法,这两个方法中因为电梯上升下降和楼层中乘客上升下降需求的可能性比较多,也使得各种条件判断起来的难度较大,第一次的bug就是出在这里。
这两个方法应该有简化的可能,寻找上升下降的相关性,写成一个函数然后分别调用,也可以把不同条件的判断细化到多个方法。
时序图
main的创建
电梯时序图
心得体会
线程安全
这次锁的东西比较多,为了线程安全问题把很多方法和对象都锁过了,并未出现过死锁问题,但是出现了线程分配导致的性能问题。由于锁的唤醒只能唤醒被这个锁锁住的线程,之前写的时候因为理解不到位有的东西没被唤醒de了好久bug。
所以说不要乱锁,先好好判断一下需要锁什么东西,把必要的有线程安全问题的东西锁住了,过犹不及。比如这次为了避免电梯间对于队列的线程不安全,只需要锁住楼层队列就可以了,但我还锁住了楼层进出人的方法,就显得有些多余了。
层次化设计
其实这次设计的类比较少,结构也很简单,但是一个设计用了三次作业,因为这种设计方式十分符合我自己的思路。即把所有电梯自己可以判断的东西都放在电梯内部,电梯内置了一个楼层组容器,目标楼层的设置,开关门判断,进出乘客,上升、下降,都由电梯自己判断。这样的结果是可能导致电梯类内部过于复杂。而剩下电梯之间的调度主要是由调度器类分配各个电梯的需求队列来实现,这也导致换乘之类的操作比较难以进行。
如果需要进行换乘操作的话,应该设置换乘楼层,在调度器分配乘客的时候,如果乘客的到达楼层不为该电梯可达楼层,则将该电梯目标楼层设置为换乘楼层。
我看到一个同学的思路为B类型电梯可以将起点为奇数层而目标层为偶数层的乘客,运送至目标层相邻的奇数层,这是一个可取的思路,只需要在设置目标楼层时改为若乘客目的地为偶数层,则运送到该层-1。
总体心得
这次没有进行重构也是非常happy,尤其是把各个功能具体分散到不同的方法中,避免了出现功能较多的方法,这样的设计比较适合扩展。也正因如此,第二次第三次作业消耗的时间都非常少,只是在第一次架构的基础上进行少量的拓展。最主要的时间还是花在第一次作业中对多线程的理解与掌握上。