OO多线程单元总结博客
OO第二单元总结博客
一、大体架构
本单元的三次架构有些不同,因此同步块和锁的设置也不太相同。
1.第五次作业
第五次作业的架构并不复杂,我设计了一个请求队列WaitList用来保存乘客请求。输入线程作为生产者,每一架电梯作为消费者。因此,本次作业的锁即选择为主请求队列。同步块的设置为:对于输入线程,当从标准输入读入一行表示新请求的时候,对WaitList加锁,向队列里面加入一个请求,之后唤醒所有线程。而电梯要做的事情是,每到一个新的楼层,把WaitList锁住,并且进行读取,如果有可捎带的请求,那就把WaitList中的可捎带请求去掉,将其加入到电梯自己的loadedRequest中,实现载人操作。输入线程相当于写进程,对锁有写入操作。电梯线程相当于读写线程,先对锁进行读操作,然后根据需要写数据。另外,如果电梯到一个新楼层后发现主请求队列中没有人且电梯中也没有人,那就进入阻塞状态,等待被唤醒。
2.第六次作业
第五次作业我做的事情太简单了,因此欠了一些债,不过第六次作业大体把框架都补上了。
第六次作业相对于第五次作业加入了调度器线程。调度器线程用来将主请求队列中的每一个请求分配到每一个电梯的次请求队列OperateList当中。
本次作业的一个条件是,次请求队列的最多容纳个数不超过电梯最大承载人数。
因此,本次作业设置三类锁,一个是主请求队列的锁,一个是每一个电梯的次请求队列的锁,还有一个是有关整个电梯操作队列的锁。
当标准输入有新的一行请求时,输入线程将WaitList锁住,并且对其进行写操作,向主请求队列中加入新的请求,并且唤醒在此阻塞的所有线程。而调度器线程每一次调度的过程中,锁住WaitList,读取其中元素的数量,若为零则进入阻塞状态。当被唤醒后,继续运行。首先判断OperateList是否满员,若是则进入阻塞状态,等待被唤醒。被唤醒后,依次读取每一架电梯的方向,楼层,状态参数,操作队列,并且将主请求队列中的请求搬运到电梯的此请求队列中。而对于电梯线程,每到一个新的楼层,先锁住次请求队列进行读操作并且判断是否需要开门,若不需要开门直接走。若需要开门则开门。之后再锁住次请求队列做写操作(上下人),之后关门,重新制定移动方向。如果电梯次请求队列有被完成的操作,则唤醒在此等待的相关线程。
3.第七次作业
第七次作业的同步块和锁的设置与第六次作业相同,在此不多赘述。
二、调度器设计
1.第五次作业
第五次作业没有进行调度器设计。
2.第六次作业
第六次作业调度器从WaitList主请求队列中拿请求,并且分配到电梯次请求队列OperateList中。交互方式是先判断WaitList是否有东西,如果WaitList没有东西,那就进入阻塞态,等待输入线程抛出请求并将其进行唤醒。如果被唤醒之后WaitList依然没东西,就表示输入结束,将请求调度完之后关闭线程。如果WaitList有东西之后,判断OperateList是否已满,如果是,则进入阻塞状态。此时,对于电梯线程,当每从电梯下一个人的时候,通过OperateList唤醒在此阻塞的调度器线程。再次被唤醒之后,调度器锁住WaitList和OperateList开始按照一定算法调度。
本次调度的算法是:锁住电梯并且读取电梯的状态,楼层,运行方向。优先给闲置电梯分配尽量同方向的请求。其次将请求分配给可捎带请求的电梯。Random模式来一个请求调度一个,Morning模式是等OperateList被分配满或者输入结束时才唤醒电梯线程,Night模式一次将请求都分配满。
电梯的运行逻辑很简单。他要保证运行方向能不变就不变。比如说,当电梯内部还有人要去的方向与之前最近时刻的运行方向一致,电梯就不换向。如果电梯内部所有人都下的话,看次请求队列第一个请求要去的方向,以此决定方向。
3. 第七次作业
第七次和第六次作业的调度器设计实际上比较类似。或许在算法层面有略微的不同,不过大体上交互方式是一致的。
偶有不同的是,第七次作业允许换乘,因此。电梯线程也会像WaitList请求队列中加新请求。由此可见,关于WaitList,电梯线程和输入线程是生产者,调度器是消费者。而关于OperateList,调度器线程是生产者,电梯线程是消费者。
本次作业的调度算法相较于第六次作业没有太大区别,仅仅是关于处于电梯中的请求,把需要用的目的楼层换成中转楼层。
三、分析架构
下图为UML图,极其群魔乱舞:

以下是本次作业的UML顺序图:

本次作业的Scheduler类确实承担的任务比较繁重。现在考虑拓展性:
- 从性能角度分析,我本次作业性能优化的代码量复杂度为O(N),而不至于像很多人考虑多次换成而出现O(N^2)甚至更高的复杂度。所以,如果单从加入一个新电梯种类的话,我代码的修改量应该不是很大。
- 不过确实,虽然没有几何量级的分类讨论,我调度器部分很多还是写的耦合在一起,加一种新电梯确实很令人头疼,需要在很多地方加一点代码。
- 从功能方面,需要改动的部分不多,仅需要改一下调度算法(那属于性能了),不过电梯的具体行为我已经限制好,每次加新一种电梯的时候只需要改一下属性。
四、bug分析
这三次作业的互测和公测并没有被查出来任何bug。不过,自己在调试过程中吃了不少苦头,在这里讲一讲。
调试过程中出现的bug:
- 电梯运行模式出现问题,当闲置电梯遇到一个请求,且运行到该请求出发楼层的方向与完成该请求过程中的运行方向不一致时(比如一层电梯遇到了一个从三层到二层的人),电梯不会转向。问题在于Elevator线程的NeedToOpen方法,判断是否需要开门。我没有考虑周全造成逻辑错误。
- 因CPU轮询造成的CTLE问题。其实,在第六次作业中,调度器线程阻塞的条件是主请求队列为空或者次请求队列已满。在中间的情况是可以进行分配的。但是,在第七次作业中,即使主请求队列不为空,次请求队列没有满,依然有概率调度器无法进行任何分配。因此,调度器一圈一圈走,不进入阻塞状态,造成轮询。此bug造成的地点是调度器Scheduler类的run方法。我对此的解决方案是写一个函数,判断是否能调度成功,如果不能调度成功,则进入阻塞状态,且需要由电梯改变状态后持有一把特定的锁SchedulerLock来唤醒
- 一个人反复上下同一架电梯,该电梯反复开关门。出现原因:搭乘C号电梯时,参数设置不合理,在C号电梯不能将其直接送往目的地的条件下,从C号电梯下来后依然满足搭乘C号电梯的条件,因此反复出进C号电梯。出现bug的方法,Scheduler类的canAssignToC方法。改正方案,修改相关参数,搭乘C号电梯进行中转的乘客的请求的所需里程数必须大于特定值。
- 其实,作业是存在公测和互测均没有被发现的问题的。问题就是我的电梯的状态,楼层,运行方向等基本参数并没有做到线程安全,主要是怕加太多锁会造成死锁(也确实造成了),于是就把这些锁都去掉。我这种偷懒的行为是不可取的。不过这也启示我们:在做好线程安全的同时不要让自己的锁太过复杂,免得造成死锁等奇奇怪怪的问题。
以下是第七次作业的复杂度较高的方法圈层分析表:
| method | CogC | ev(G) | iv(G) | v(G) |
|---|---|---|---|---|
| Scheduler.singleAssign(PersonRequest) | 7.0 | 6.0 | 6.0 | 6.0 |
| Scheduler.run() | 27.0 | 4.0 | 13.0 | 13.0 |
| Scheduler.canAssignToC(PersonRequest) | 10.0 | 1.0 | 1.0 | 11.0 |
| RandomScheduler.requestAssert(Elevator,PersonRequest) | 5.0 | 4.0 | 6.0 | 6.0 |
| RandomScheduler.assignToPrepareElevator(PersonRequest) | 28.0 | 9.0 | 7.0 | 10.0 |
| RandomScheduler.assignToMovingElevator(PersonRequest) | 7.0 | 4.0 | 4.0 | 5.0 |
| RandomScheduler.assignToEmptyElevator(PersonRequest) | 6.0 | 4.0 | 3.0 | 4.0 |
| RandomScheduler.assign() | 9.0 | 5.0 | 4.0 | 7.0 |
| NightScheduler.assignToElevatorC() | 18.0 | 9.0 | 6.0 | 9.0 |
| NightScheduler.assignToElevatorB() | 15.0 | 4.0 | 6.0 | 7.0 |
| ElevatorB.tempToFloor(PersonRequest) | 9.0 | 4.0 | 4.0 | 4.0 |
| Elevator.run() | 26.0 | 5.0 | 8.0 | 12.0 |
| Elevator.needToOpen() | 25.0 | 10.0 | 8.0 | 12.0 |
类复杂度:
| Class | OCavg | OCmax | WMC |
|---|---|---|---|
| Elevator | 3.3333333333333335 | 11.0 | 50.0 |
| ElevatorA | 1.0 | 1.0 | 1.0 |
| ElevatorB | 2.5 | 4.0 | 5.0 |
| ElevatorC | 2.0 | 3.0 | 4.0 |
| InputThread | 2.6 | 7.0 | 13.0 |
| Main | 4.0 | 4.0 | 4.0 |
| MorningScheduler | 2.6666666666666665 | 5.0 | 8.0 |
| NightScheduler | 5.166666666666667 | 9.0 | 31.0 |
| OperateList | 1.5 | 3.0 | 12.0 |
| RandomScheduler | 4.142857142857143 | 9.0 | 29.0 |
| Scheduler | 2.5 | 8.0 | 35.0 |
| WaitList | 1.0 | 1.0 | 7.0 |
可以看出,绝大多数复杂度较高的方法都来自于Scheduler及其子类。因为他要读取WaitList,OperateList,电梯的各种属性参数,出bug的概率也相对更高一些。
其实Elevator方法复杂度也比较高,在其中,我使用有限状态机进行状态转移,并没有使用状态模式,使自己的代码看起来并没有那么面向过程。
五、互测策略
1.第五次作业
第五次作业,评测机还没有完成,无法进行黑盒测试。我可能看别人的代码看不太懂,因此也没有进行白盒测试。所作的事情仅仅是向评测系统交了一个很简单的点,来彰显一下存在感,来看一下我对于互测规则了解是否有偏差,很正常,该点没有查到任何人。
2.第六次作业
第六次作业,搭了一个串行测试的评测机,运行效率并不十分令人满意。而且,本评测搭建时间比较晚,导致并没有来得及测到别人的bug,互测就停止了。不过,互测结束后,改进了一下评测机,测出了两个人有bug,应该都是因某些原因无法结束线程产生的RTLE问题。
构造测试样例的方法仅仅是通过程序随机生成,质量并不是很高。也没有手动构造样例进行定点爆破。
3.第七次作业
第七次作业,把评测机改成并行测试,运行速度比较令人满意。在本地测出了四个人的bug,其中一个人的电梯在某些特定情况下没有换向,导致突破了1-20层的限制,估计是电梯的运行模式出问题了。还有三个人有线程安全问题造成的超时。看某一个人出现了空指针异常,估计是关于电梯人数读取有问题。不过,再交平台的时候,因为多线程的线程安全本来就是一个概率问题,并没有太多成果,只把那个电梯突破天际的人给刀了。
构造样例依然使用纯随机的傻瓜方法,没有进行定点爆破等等,美中不足。
依然没有进行白盒测试,看不下去别人的代码orzzz
六、心得体会
- 这一单元我设计的锁太过繁多复杂,很容易出现死锁等问题。另外,要保持高内聚低耦合的习惯,可以尽量多设计一些线程安全的类,这样看着也比较舒服。
- 自己的程序应该是有线程安全的问题,因此,如何在保证线程安全又要保证不出现加锁过复杂而造成的死锁问题还是值得好好研究一番的。
- 本单元作业并没有太过注重性能优化。其实,把一个请求分成多段会增大中转时间,可能得不偿失。
- 很多人都用到了像单例模式,观察者模式等等。本人并没有太过关注,应该检讨自己。好的代码风格也能方便自己调试,也方便别人阅读。
- 我身边的人都很强,我要向他们学习,学习好的代码风格,学习习惯,时间管理等等。
- 第三单元OO加油啊!!!
浙公网安备 33010602011771号