OO-第二单元作业总结
OO-第二单元作业总结
作业内容介绍
OO第二次作业的内容是实现一个多线程的目的选择电梯调度控制系统及其配套模拟程序,这个单元的训练主要目的是多线程软件的设计使用以及线程安全相关问题.
关于多线程程序设计与线程安全
这周的作业是关于多线程与线程安全的,首先先是分享我对多线程问题的一些简单认识.
首先,多线程能完成的事情,单线程都可以做到.多线程的存在,一般是为了将一部分可并行执行的任务从原有的一条串行执行队列中独立出来,获得更高的性能.这些并行,实际上是可以分为多个级别的并行的.只考虑最极端的两种,有纯并行形式的,如渲染器程序中的每一个采样,是各自完全独立的,渲染器中的每一个采样各自独立,各干各的活,这是完全并行的任务,纯并行的任务,不存在线程安全的问题.也许执行顺序会不同,但实际上纯并行任务对于执行顺序也理应是不敏感的,线程不管怎么执行,什么时候提交,都是相互无关系的.另一种极端的并行,是将纯串行的任务拆成多个流水段以并行执行.这更像cpu中的执行流水,一级做完交给下一级做.这时候,线程之间是完全不独立的,容易出现各种冲突造成的线程安全问题.
在这种流水并行的场景中,首先需要解决的问题是速度匹配问题,上级流水线处理的速度不一定赶得上下一级消耗的速度,下一级消耗的速度也不一定赶得上上一级处理速度,这就引出了比较经典的生产者消费者模式.实际上抛开名词来说,这就是借助先入先出的队列实现了一个不同流水之间的速度匹配.在速度匹配的过程中,有可能出现队列首尾重叠或者同时多个读队列造成的线程安全问题,我们写程序时候,对于多线程,实际上只需要仔细注意好这一部分,就一般不会出现线程安全问题了.
同步块设计
正如上所言,我的程序中,使用了多级的消费者生产者模型,每一级之间全部使用了队列进行连接,所以我的锁只需要加在这些队列中即可.我的同步块,就是所有的队列操作,保证同时只会对队列进行一步操作,如写入或者读出.
在我的程序中,有两个单独的队列类,分别是有锁队列和无锁队列,两者的所有访问方法都是加了同步锁的(实际上可以对于不同的队列属性加锁),在有锁队列中,当有线程在获取队列中的对象时,发现队列为空,就会进入wait等待.在下一次队列状态更新时,所有等待线程会被唤醒去竞争锁.
在输出部分,由于题目对于输出顺序有要求,不得不将输出也包装成为线程安全类,方法就是建立一个wrap类,wrap原本的输出方法为同步方法.
使用这种设计,保证了程序中其他部分不用单独加锁,或者单独加锁只能起心理作用,限制了冲突发生的范围.
调度器设计
我设计的调度器比较naive,对于每一个单部电梯可以完成的请求,若 存在多部电梯同时可以满足请求,则将请求按顺序分配给每一个电梯.对于每一个电梯,在自己的请求队列中,使用ALS策略.对于一部电梯无法满足的要求,我会对其进行路径规划,这个路径规划是简化的路径规划,预先假设了路径只能有三大段,避免了最短路算法.程序会遍历每一个可能的路径组合,根据电梯目前搭乘乘客的数量和电梯移动速度,容量等,计算一个期望移动时间,每一个乘客请求都会按照期望最短的路径去发送.
在交互方面,首先inputManager将请求送入路径规划器,规划之后开始发送,分别送入每段的楼宇/楼层总控,由楼层/楼宇总控均匀分配给可达电梯执行.这些过程全部借助有锁队列完成.
UML类图
由于我三次作业的架构上并没有非常大的改动,这里以第三次最终版本的架构图进行介绍.
本程序使用了多级的生产者,消费者模型.实际上,对于每两类有执行顺序关系的线程,我都为其加入了一个同步队列类或者无锁队列,用于协调.得益于这种设计,本程序的共享区仅仅存在于这些队列中,只要设计好这些对象队列的线程安全,整个程序大体上也不会有问题.对于两种不同特性的线程,设计了无锁队列与同步队列两种,其中同步队列会在队列空时,锁住调用线程,防止轮询发生.对于无锁队列,由于这些线程的所有者要求不能被阻塞,故使用无锁队列,避免这些队列出现轮询的逻辑在调用者方实现,具体到本题目中,就是电梯的模拟线程,若队列为空则继续运行
度量分析
Class | OCavg | OCmax | WMC |
---|---|---|---|
com.Person | 1.5 | 3.0 | 17.0 |
com.RouteNode | 1.0 | 1.0 | 3.0 |
com.VerifyFloor | 2.0 | 2.0 | 2.0 |
elevator.Elevator | 8.0 | 27.0 | 32.0 |
elevator.ElevatorCtrlUnit | 2.6 | 12.0 | 24.0 |
elevator.ElevatorInfo | 3.1 | 14.0 | 19.0 |
elevator.ElevatorPlaner | 3.1 | 9.0 | 19.0 |
elevator.ElevatorPlanerFifo | 1.3 | 2.0 | 12.0 |
elevator.FloorMoveInfo | 1.0 | 1.0 | 6.0 |
elevator.FloorNode | 1.0 | 1.0 | 3.0 |
elevator.FloorPlaner | 2.8 | 5.0 | 17.0 |
fifo.NoInterlockFifo | 1.0 | 1.0 | 4.0 |
fifo.SynchronizedFifo | 1.3 | 3.0 | 8.0 |
input.InputManager | 7.0 | 7.0 | 7.0 |
Main | 3.0 | 3.0 | 3.0 |
outputwrap.OutputWrap | 1.0 | 1.0 | 1.0 |
router.FloorMoveInfoRet | 1.0 | 1.0 | 3.0 |
router.Router | 3.6 | 6.0 | 11.0 |
Total | 191.0 | ||
Average | 2.3 | 5.5 | 10.6 |
从度量分析的结果来看,Elevator类(电梯执行的模拟器),执行逻辑是一个大循环,状态较多复杂度高,InputManager对输入请求发送并发送到不同的处理模块,复杂读较高,Router负责路径规划,复杂度也比较高.
个人bug分析
在第一次作业中,评测中出现了一个比较难复现的线程安全问题,仔细研究后发现是由于队列输出结束信号时,未检查队列是否为空造成的,在一些比较特殊的,等待队列先收到请求结束等待,之后马上发送停止信号的情况下会出现.
第二次作业强测以及互测中未发现bug
第三次作业中,先是出现了一个执行顺序问题造成的bug(乘客下电梯后,先将其返回了等待队列才输出离开电梯信息,导致极端情况下,人会先输出进入下一段电梯再输出离开本段电梯),之后是由于性能问题造成的rtle,这估计是由于将同楼座(竖直方向)电梯请求直接平均分配造成的.
发现别人bug所采用的策略
实际上我没有通过任何方式去尝试发现自己或别人的bug
心得体会
- 多线程的使用:通过这次作业,对于流水线形式的多线程使用有了一个比较初步的认识,使用java编写多线程程序,要比c/cpp语言舒服不少,在c/cpp中使用unix中的pthread库,总会觉得比较复杂麻烦,使用openmp这种程序编写的多线程程序限制也比较多.
- 性能优化:本以为最保守,按照教程来的策略不会出问题,实际上在第三次作业中还是由于性能低的过于离谱导致出现了rtle.这次的三代作业中,实际上确实基本没有花时间去优化性能,调试问题,没有特别注意这方面,没想到真会卡rtle.