北航面向对象Round Two
1.三次作业设计策略
1.1第一次作业设计策略
第一次作业要求写出一个单部多线程傻瓜调度电梯的模拟。因为这次作业,是我们大多数人第一次写多线程作业。所以代码的逻辑难度较低,但是如何写出一个多线程的程序,对很多人来说都是一道难题。
第一次作业主要遇到的问题是如何做到线程间共享变量的访问互斥,如何使用wait和notifyAll这两种函数实现进程同步。在仔细阅读了老师的PPT和网上的一些信息以后,我大致理解了如何写一个多线程程序,尤其是写一个生产者-消费者的多线程程序。第一次作业主要有两种架构,一是两线程架构,即输入接口为生产者,电梯为消费者;另一种架构是三线程,除了输入接口和电梯以外,还有调度器为第三个线程,与前两个线程做到同步。因为三线程程序过于复杂,所以第一次作业我采用了两线程的方式。
输入接口负责读取数据,电梯线程负责拿取数据运行,他们的线程间共享变量是一个指令序列,指令序列由调度器保管。则在调度器中访问进程间共享变量的方法都需要使用synchronized进行保护,防止数据访问冲突。并且,由于两个线程需要同时访问同一个调度器,需要保证调度器的唯一性,即单例模式。我使用的方法是在主线程中创建调度器,并将其作为输入接口和电梯的构造方法传入两个线程中,这样就保证了调度器的唯一性。
此外,在共享区域为空时,电梯会wait。当有指令传入输入接口时,输入接口会notify电梯,通知它该干活了。如此做到进程的协同。
本次作业的UML类图如下:
由上图我们可以看到本次作业只设计了两个线程,两线程的优点是按照生产者和消费者的写法可以迅速完成任务。但是在实际的代码实现中,并没有使用final保护共享变量,使其由修改的可能,会给以后的代码书写带来隐患。并且,本次作业是硬编码,扩展性较低。
1.2第二次作业设计策略
第二次作业要求实现可稍带的电梯,并且对电梯的楼层做出了调整,出现了负楼层。除此之外没有难度的增加。由于有了第一次写多线程程序的设计经验,本次作业我继续延用上次的两线程设计方案。为了追求性能,我采用了磁盘扫描的调度算法,并且这个算法思想较为简单。我针对电梯的每一个楼层设计了一个楼层的进队列和出队列。进队列表示发出要进入电梯请求的人,出队列表示现在已经进入电梯,准备到某一楼层出来的人。当电梯扫描到某一楼层,如果发现这一楼层的进队列或者是出队列不为空,则电梯需要开门,再根据当前层进队列或者是出队列的情况判断所要执行的操作。这样就需要把整个楼内,所有楼层的进队列和出队列作为一个共享变量,而两个线程要做到访问互斥。
同样的,本次的共享变量仍在调度器中保存,则两线程对调度器中共享变量的访问都需要加synchronized保护。在线程之间的协同方面,输入接口仅负责往共享变量中存放数据,而电梯则对共享数据作一系列的访问。所以两线程架构实现起来较为简单,很少出现访问冲突的问题。
程序的UML类图如下:
与第一次作业的UML类图相比,程序的类的总数并没有改变,而是在类中增加了一些方法,说明本次代码重构效果较好,并且放弃了硬编码。但是,本次作业的building并没有储存在电梯里,而是放在了调度器中,这在第三次作业中做了改变,说明放在调度器中是不太合理的,并没有考虑之后的代码重构问题,给下次代码的coding带来了障碍。
1.3第三次作业设计策略
第三次作业难度陡然上升,不仅仅是因为有了多部电梯,而且因为有些电梯指令需要涉及到换乘。并且对电梯的载客量做了一定的限制。我仍然采用了两线程的架构,在题目要求的多部电梯上面,我设计了一个电梯类,将每个电梯不同的参数在它的构造方法中赋初始值。为了处理换乘现象,我采用了类似于“计算机网络”中转发的方式,利用一个二维矩阵求出当前指令的“下一跳”,如果“下一跳”与当前指令的目的楼层相同,则说明该指令不需要转发。如果“下一跳”与当前指令的目的楼层不同,则代表该指令需要转发,调度器会在指令内部设置关于转发的参数。当电梯执行完当前指令后,会判断指令内是否有转发模块,如果有转发模块,会将此指令重新丢给调度器。
与前两次作业不同,为了使电梯能够正常关机,我给每个电梯都设置了一个指令队列,由调度器负责分发指令,电梯仅需执行自己的指令队列即可。调度器这样也保证了电梯不会出现超载的状况。
关于共享变量的访问,本次作业同样有三个共享访问区域,分别为三个电梯的指令队列。输入接口负责把接收到的指令放置在里面,而调度器负责将收到的指令分发给三个电梯,三个电梯之间不会产生联系,各自独立完成自己的任务。
本次作业的UML类图如下:
由上图可知,本次作业仍为两线程架构,但是本次作业的共享区域并不是building,而是调度器中的请求队列。这样的好处是共享区域变的简单,程序的临界区代码复杂度也就变少了,程序的并发效果的颗粒度就更细了。但是本次作业仍有不足的地方比如没有追求性能部分,而且对SOLID的要求做的并不到位(这个之后会详细说明)。
2.程序度量
2.1经典的OO度量
2.1.1第一次作业的度量结果
度量对象 | 度量项目 | 值 |
---|---|---|
全部工程 | LOC(代码长度) | 157 |
Dispatcher类 | LOC(代码长度) | 21 |
InputDevice类 | LOC(代码长度) | 36 |
Instruction类 | LOC(代码长度) | 20 |
Lift类 | LOC(代码长度) | 70 |
LiftSystem类 | LOC(代码长度) | 10 |
Dispatcher类 | 方法数 | 3 |
InputDevice类 | 方法数 | 2 |
Instruction类 | 方法数 | 4 |
Lift类 | 方法数 | 5 |
LiftSystem类 | 方法数 | 1 |
从度量来看,第一次作业并不复杂,仅有百行之多。更多经典度量结果如下:
可以看出,第一次作业的耦合度适中,且类和方法并不复杂,效果较好。
2.1.2第二次作业的度量结果
度量对象 | 度量项目 | 值 |
---|---|---|
全部工程 | LOC(代码长度) | 278 |
Building类 | LOC(代码长度) | 28 |
Dispatcher类 | LOC(代码长度) | 85 |
Elevator类 | LOC(代码长度) | 98 |
InputDevice类 | LOC(代码长度) | 38 |
LiftSystem类 | LOC(代码长度) | 10 |
PersonData类 | LOC(代码长度) | 19 |
Building类 | 方法数 | 6 |
Dispatcher类 | 方法数 | 10 |
Elevator类 | 方法数 | 7 |
InputDevice类 | 方法数 | 2 |
LiftSystem类 | 方法数 | 1 |
PersonData类 | 方法数 | 4 |
与第一次作业的度量结果相比,第二次作业明显比第一次作业复杂一倍,原因是第二次作业要求对电梯的调度进行优化,为了完成我的优化算法,所以我写的稍微唱了一些,并且调度器的方法数是所有类中方法数最多的。下面看一下其他度量数据结果:
有上图可以看出,虽然需求变多了方法复杂了,但是方法的耦合度还可以,整体效果还可以接受。
2.1.3第三次作业的度量结果
我们直接看第三次作业所有数据的度量结果:
其中,代码总行数已经达到了568行,每个类中的方法数量也几乎翻了一番。可以说这三次作业的复杂程度呈几何状态增长。但是第三次作业使用了大量的Magic Num,这是在对电梯所能停靠楼层部分输入的结果。
2.2程序之间的协作图
2.2.1第一次作业的协作图
第一次作业协作较为简单,仅仅为只有一个生产者和一个消费者的模型。
2.2.2第二次作业的协作图
由于改变了调度算法,并且新加入了一个共享访问区域,楼层,并且电梯只针对楼层服务数据,所以第二次作业的协作图如上图所示,仍采用的是两线程的方案。
2.2.3第三次作业的协作图
第三次作业有三个电梯,并且会有人员的换乘,但是每部电梯之间并没有交集,他们只与调度器交互。这种设计不容易发生死锁现象。
3.程序中出现的bug分析
3.1第一次作业存在的问题
第一次作业中,我中测出现了一个严重的bug。在第一次提交代码后,我发现我的中测结果全为TLE。后来排查原因发现是线程没有正常结束。原本我以为只需要main函数正常结束就OK了,结果发现要所有的线程全部结束,程序才会正常退出。本地输入空行可以使用ctrl+D实现。解决了这个问题之后,代码在中测和强测就没有出现问题。
3.2第二次作业存在的问题
第二次作业中,同样是中测全部TLE,因为我在写代码的时候注意了电梯关机的情况,并且本地测试也没有问题。在我后面仔细排查后,发现还是电梯关机的问题,这个bug仅会在指令输入完毕后立马输入空行才会复现。而在指令输入后停止一段时间再输入空行,电梯则不会出现这样的问题。原因是我的电梯线程再判断退出条件的时候出了问题。在修复了这个bug后,我再次提交,之后的中测和强测也没有出现问题。
3.3第三次作业存在的问题
由于第三次作业我没有追求性能分,仅仅完成了指导书中所提出的必要任务,所以我的代码逻辑较为简单。中测提交一次全部通过,在强测也没有出现问题。然而在中测提交之前,课下debug的时候,我发现了一个多线程中容易犯的问题,就是当有一个线程获得了一个数据的锁时,它再去获得另一个数据的锁,此时及其容易出现死锁现象。在写代码时遇到此类现象要格外小心,防止死锁现象发生。
3.4自己的debug策略
一开始debug是插桩判断某个数据在哪个地方输出异常,在通过中测之后,我就以画协作图的方式,判断何时会出现共享变量的访问冲突。然后检测那个地方的代码是否出现问题。很显然,这个检测的方法是比较有效的,它能帮助你理清代码的逻辑,并将关注的中心放置在多线程程序容易出错的地方。
4心得体会
本次作业在实验过程中并没有出现死锁现象,但是注意到了写代码时可能产生死锁的地方。多线程的代码难以调试,大家一般采用的方法时插桩。但最有趣的是,当你插桩前你的代码有bug,在你插桩检查之后,你的代码中的bug却又消失了。我也终于明白了为什么说能够复现的bug都不是bug而不能复现的bug才是真正的bug。
单例模式是本次作业最常用的一个设计模式,因为我们只需要一个调度器来管理全局的电梯。本次作业始终没有尝试三线程架构,一开始是因为觉得三线程会很难,但是写到后面才发现,三线程与两线程相比,可能并没有提升太大的难度。
最难受的是我的电脑在我把博客写了2/3的时候,电脑的主板烧了。我枯辽。。。。。。又重新写了一遍。
😢 😢 😢 😢 😢 血亏一千大洋。