OO第二单元实验总结报告
一. 同步块与锁
在本人的架构当中,共享对象是Controller
,或者更准确的来说,是其中的waitQueue
。在输入线程requestInput
向其中添加请求,而Controller
通过计算将相应的请求分发给Elevator
。在这个过程中涉及到了对waitQueue
的添加、遍历与删除,所以对它的操作需要考虑线程安全问题。首要是在对它操作时要用syncronized
块包裹,来确保读写的安全性。除此之外,因为涉及边遍历边删除的操作,需要用迭代器来保证操作安全。然后是对象的wait()
、notifyAll()
。我们的Elevator
线程中当无法获取请求而输入尚未结束时要停下来释放锁,即wait()
,与之相对应的,当我们添加请求或者是输入结束时(这正好对应前面的判断条件),也要相应的notifyAll()
来唤醒线程。
另一个不太显然的同步块是相对于TimableOutput
的,每次输出时都需要对这一资源上锁来确保输出的线程安全性。
二. 调度器设计
第一部分中我们已经针对各模块协作来描述了调度器与线程的交互问题,这部分将详细描述并且叙述调度策略。在我的设计中为每一层、每一座都添加了相应的调度器,共计15个。
在我的设计中,requestInput
与调度器的交互是靠addRequest()
进行的。我们将新输入的请求放入调度器以供之后处理。Elevator
与调度器的交互有几个不同的方式。当电梯到一层时,它需要与相应调度器沟通来将要上电梯的人从调度队列放入电梯队列,当电梯需要确定方向时,它会向调度器获得以获得相应的请求位置,来决定自己移动方向。
调度策略则是决定上面所述两种情况具体应当返回哪些请求。我采用的是LOOK算法以及相应捎带策略。也即到一层时,我们让请求方向与电梯运行方向相同的人上电梯(当然也要注意电梯容量问题),而决定方向时我们以调度器等待队列的首个元素来决定方向(这类似于ALS策略,但我们没有明确的主请求)。这样简单的策略被证明效率也较高。
第二次作业中增加了电梯数量,引入了多电梯共享调度器的协同问题。我才用了比较方便的策略,也即自由竞争方法。我们不去更改调度器,和上面所述保持一致就可以过第二次作业了。这是由于电梯会竞争上岗,而对waitQueue
的上锁保证人只会上一个电梯。在操作中这回造成一定的时间损失,因为电梯本可以为靠后的指令服务,而现在都去抢靠前的人员了。总结过上来看这个策略在平均结果下表现得不错。
第三次作业引入了更复杂的请求,为了划归问题,我采用了教程中所述的拆分方式,使指令的类型划归成为了第二次的类型,并一次性加入到调度器当中。当然,由于这几个分拆指令是具有时序关系的,我们为它设置了一个flag
来表示相应部分能否执行,并且有方法来为下一条指令的flag
置真。此时调度器与电梯的交互方法就要做相应的修改,因为需要考虑一个指令当前是否有效。最后只需要在一个指令离开电梯后将后继指令便有效即可。注意到此时我们还需要对后继指令对应所在的waitQueue
进行notifyAll()
的操作,这就要求我们能够基于一个请求找到其所在的队列。我因此设计了一个单例模式的BigController
来统筹所有15个调度器,并通过它来找到相应的队列。
还有线程的结束问题,第三次作业中分拆指令的操作导致电梯线程的总结发生了改变。为了适应这一变化,我在每一个电梯线程的结尾处加入了基于BigController
的将所有队列唤醒的操作,这样就可以保证所有线程都顺利结束。
这个策略看似简单,但最后性能分出乎我意料的高。
三. UML图及分析
下面是主要类与方法的度量分析
method | CogC | ev(G) | iv(G) | v(G) |
---|---|---|---|---|
BigController.awakePerson(Person) |
6 | 5 | 5 | 5 |
Controller.checkIn(int, VerticalDirection, int) |
7 | 5 | 3 | 6 |
Controller.getDirection(int) |
18 | 9 | 7 | 10 |
Controller.getIn(int, VerticalDirection, int) |
9 | 5 | 4 | 7 |
Controller.hasPerson(int, VerticalDirection) |
17 | 9 | 5 | 9 |
Elevator.run() |
33 | 6 | 12 | 14 |
HorizontalController.checkIn(char, HorizontalDirection, int, int) |
7 | 6 | 2 | 6 |
HorizontalController.getDirectionFrom(char) |
11 | 7 | 6 | 7 |
HorizontalController.getIn(char, HorizontalDirection, int, int) |
9 | 5 | 4 | 7 |
HorizontalElevator.run() |
41 | 6 | 15 | 17 |
RequestInput.run() |
14 | 3 | 9 | 9 |
可以看到耦合最为严重的就是几个线程的run()
方法,这是由于它们都带有循环判断与对各种对应的其他类方法的调用。其他几个方法都是为了判断方向或能否上下电梯而做出的判断语句。
class | OCavg | OCmax | WMC |
---|---|---|---|
BigController |
3.71 | 7 | 26 |
Controller |
3.9 | 10 | 39 |
HorizontalController |
5.0 | 7 | 5 |
HorizontalElevator |
2.4 | 12 | 36 |
RequestInput |
4.5 | 8 | 9 |
四. 自己的bug
前两次作业都顺利的通过了强测也没有被hack,但这并不表示没有bug了,在第三次作业中发现了前面的遗留问题。部分对waitQueue
的操作忘记了上锁,导致可能发生ConcurrentModificationException
,这个错误的诱因是迭代开发时忘记了对新的方法上锁。然后是第三次作业被hack掉了。在本地靠循环内输出来debug时发现有时候会出现当请求已经从waitQueue
到电梯队列了,并且waitQueue
已经为空。但此时电梯仍然试图从waitQueue
中获取方向,此时获得的方向是STOP
,这会导致死循环的发生。由于这个bug对调度顺序有一定的依赖性,我自己并没有发现。修改中只需添加一层判断,当上面情况发生时,以电梯自己队列为标准定向即可。
五. 课下debug心得
和上一栏目类似,前两次作业都比较顺利,而第三次作业则不然,一上来就是CPU_LIMIT_EXCEED。到至这个Bug发生的常见原因便是轮询与死循环,于是我便在每个循环中添加输入语句来便于debug。最后结果表明是轮询的问题。原因是在迭代开发过程中,一些条件的判断发生了变化。之前是只用看调度队列是否为空,现在由于加入了flag
为false
的请求,于是对空的判断也应抛开它们。这样修改checkEmpty()
后就解决了。
我在自己的测试当中采用了随机生成的策略,并且我也没写validator
,我基本上对自己代码的正确性还是有信心的,只是担心线程安全相关问题的出现,如死锁或等待。而这只用看代码能否终止就行了。测试起来比较方便。
六. 心得体会
线程安全问题的本质在于共享对象的处理,在设计当中我们就需要明确共享对象是什么,并对相应对其的操作上锁。在我自己的代码经历中,常见的Bug原因便是写着写着就把这茬忘了,然后就导致对共享对象的读写不一致了。所以在设计伊始就要明确共享对象是什么,并在检查时由重考虑这方面的问题,由于与之相关的Bug往往与线程调度有关,难以通过黑箱测试发现,这更要求我们在代码书写阶段就思考完善。
层次化设计方面要注意抓住事物的本质。实验中有横向纵向两种电梯,我的设计也相应有两种调度器,它们是十分相似的,设计时可以采用面向对象的思维来增加代码的复用。当需要全局规划时,我也可以相应的构造管理所有对象的类,来完成全局的改变。当然我自己的程序在这一方面是有所不足的,从上面度量分析也可以看出各种类之间耦合较高,这在处理像作业这种总体来说改动不大的项目时还行,但对于更剧烈的业务变化就难以处理了。这个苗头在第三次任务中就有出现。
总的来说,这次作业让我对多线程设计从无到有的学习到很多,也在实操中用到了这些想法,受益匪浅。