经过第一单元作业的训练,在做第二单元的作业的时候,要更加的有条理。但是第二次作业多线程的运行,带来了更多的运行的不确定性。呈现出来就是程序会出现由于线程安全问题带来的不可复现的bug。本单元的作业也让我更加认真的思考了性能和架构之间的关系,对于工程架构的设计有更进一步的认识.
历次作业分析和总结:
第一次作业:单电梯的傻瓜调度
类图如下:
第一次作业由于并没有性能要求并且刚刚接触多线程的运行,程序的结构简单并且多线程的各个线程之间并没有很多的交互。第一次作业设计中为了第二次作业的扩展,只是简单的开了电梯调度的线程,但是
只是简单的把输入的请求放到电梯运行的请求队列,没有进行其他的调度。并且由于刚开始并不熟练线程之间的交互,导致cpu的实际运行时间非常大。也就是,电梯不停止cpu一直被占用。
复杂度,耦合度,代码量表格如下:
上面表格中的信息也可以看出来,由于第一次作业的结构过于简单,并没有出现复杂度很高,耦合度高的情况,并且代码量也很小。
第一次作业中对于线程的run方法的重写过程,并没有清晰地费力出各个方法。导致run方法比较臃肿和相对复杂。
第二次作业:可捎带的单电梯调度
类图如下:
- 本次作业支持了单电梯的可捎带调度。在程序的结构设计上,这次作业要比第一次作业复杂的多。在第一次作业的基础上:
- 我增加了把请求分解成为电梯的运行请求,也就是楼层请求的过程。而每个楼层请求包含着一个该层乘客上下电梯的人员链表。在乘客类中重写tostring方法,到该楼层遍历链表输出所有上下电梯的乘客。
- 增加了一层调度器,但是并没有另外开线程。也就是总调度器下有电梯单独的给电梯输送请求,并给请求排序的调度器Floor类(这是这次设计中的一个缺点,楼层请求类和调度器直接耦合在一块)
- 电梯从第一次作业的拿到一个乘客请求变为每次只是拿到一个楼层请求,只需要拿到请求,运行到目的路层,上下乘客,只是单单执行请求。
- 关于第二次作业的线程交互和线程安全问题:
第二次作业中的线程交互问题还比较少,只是涉及到调度器和当前电梯运行之间的协调。我才用的是电梯在上下楼层和开关门的时候放开电梯本身的类锁。调度器可以访问电梯当前的运行方向和楼层信息。并且这时候电梯放弃请求链表锁,调度器可以重新安排电梯目的楼层。电梯之间的交互并没有使用notify,只使用了wait(timeout)的形式。让电梯再等待一定时间之后可以重新主动要回锁。这种方法的好处是不需要去规划notify之后哪一个线程得到锁,会出现什么结果。
- 关于第二次作业的电梯调度算法及其实现:
第二次电梯的调度采用的是look算法。从强测的结果来看中规中矩,有运行比较好的点但是也有毫无性能优化的点。算法的实现过程主要体现在两层调度器的配合。主调度器主要完成当前请求的分解(分解为上楼层请求和下楼层请求)。再根据电梯当前的状态,给出匹配度。二层调度器接受楼层请求和匹配度,进行请求的正确插入。
复杂度,耦合度,代码量表格如下:
- 度量分析
程序的度量中可以看出,主要的复杂度和依赖度高的在于调度器这一块。而由于第二次作业的第二层调度器并没有和电梯的运行楼层请求剥离开来,因此其复杂度高,耦合性比较强。从以上的度量分析中可以看出调度器的结构有待优化提高。
- bug分析及测试方法:
在公测和互测中并没有出现bug。
本次测试采用了自己打包的请求发射器和结果检查。数据集的构造并没有使用自动生成数据集。由于请求发射器存在一定的延时(0.05s左右),有时会出现构造的数据集输入后达不到预期测试效果。测试的时候构造测试用例主要针对于电梯的调度和电梯运行的线程安全问题。即是否会在一些运行的临界点出现调度问题或者是这时候出现新的调度而电梯运行错误。
第三次作业:多电梯的智能调度
类图和UMLjava类协作图如下:
- 本次作业是支持多电梯的调度,相比第二次作业:
- 最大的改变是把电梯的第二层调度器剥离出来,并且构造成了一个新的线程类。
- 增加了请求的分割过程,即解决换乘问题。本次作业中采用的是确定唯一换乘方案:总调度器在接受请求之后,确定换乘方案,会把请求放到相应电梯的乘客池。因此,换乘方案是无法更改。并且本次的换乘方案最多只会涉及到两部电梯。
- 修改第二层调度器,支持换乘请求的正确执行。在拆分出换乘请求之后,可能会出现乘客还没有到换乘地点,电梯已经输出乘客换乘上了电梯的情况。因此修改了二层调度器的运行和电梯 上下乘客的方法。并且给每个乘客类增加了是否可执行,以及是否已经执行的属性。
- 通过协作图分析:输入处理和总调度器共享原始请求队列,总调度器取出队列请求,确定合适换乘方案之后,把请求直接分解成为乘客上下的乘客类加入到二层调度器的乘客池(PassengerPool),二层调度器每次需要遍历寻找距离当前电梯楼层最近的可执行乘客类,同楼层乘客组成为一个楼层请求。合理插入到电梯运行队列。电梯直行之后,把已经执行的乘客标记为已经执行,而乘客的可执行属性取决于他的前趋请求是否完成。
- 线程交互和线程安全问题:
第二次作业的线程交互增加,也是因为线程数量的增加和电梯的限制增多。本次作业的设计和修改过程中多次出现线程的死锁:电梯拿到的请求所有乘客请求都不能执行,而调度器却总是认为该请求是当前应该执行的有利于性能的请求。因此两者都无法继续运行出现死锁。还有由于电梯容量限制导致的死锁。之后,通过修改调度算法(牺牲性能QAQ)解决了这一死锁问题。
- 本次作业的调度算法和实现:
本次作业的调度算法在look的基础上,基于程序的死锁问题进行了一些改动。由于本次作业很大程度上放到了正确性的完成(强测还是跪了一个点),性能并没有做很好的实现和优化。
复杂度,耦合度,代码量表格如下:
- 度量分析:
由本次作业的度量:调度器中存在大量的循环和判断结构,复杂度高。第二层调度器中结构并不清晰,其run方法没有实现各个过程的分离。而对passengers类修改之后,其封闭性下降,很多属性容易被修改且不易察觉。
- bug分析和测试方法:
本次作业中出现电梯的运行问题。在处理当前楼层数字为零的情况出现bug,会在同一楼层arrive两次,在强测的时候被测试出来。测试中没有出现线程安全问题。互测的时候采用和第二次相同的策略。但是这次测试过程中,重在针对于电梯超载和请求换乘方面构造测试数据。在互测中发现由于换乘的问题出现线程安全问题。
debug方法:
在多线程程序的编写过程中,由于不可复现bug的出现以及多线程的运行无法使用断点来定位bug,debug的难度有所提升。并且debug的方式也回归到了原始的打印输出定位bug。在本单元的作业中由于输出结果的检测会忽略err流的输出,所以debug的时候利用这一特点,把需要的信息输出到err流中,在不影响评测情况下可以输出运行信息并且不需要反复的注释掉输出语句。
作业的心得体会:
本单元作业的完成过程中,随着作业的复杂度不断地提升,我并没有保持很好的设计架构的清晰和独立性。并且由于第二次作业的二层调度器的设计不独立导致第三次作业在原来思路上扩展多电梯的过程中走到死胡同不得不把第二层的调度器进行重构。相比于完成第一单元作业的过程,代码重用已经有所提升,程序的可扩展性也有所提升。但是程序的架构设计仍然存在不足,并没有充分的考虑耦合性问题。作业中存在着耦合度比较高的类。这也同时造成在修改程序的时候互相影响,bug增多。架构设计过程基本遵循单一功能原则。第三次作业中的乘客类的重构破坏了开闭原则。
关于线程安全,到第三次的作业会明显感受到线程不安全带来的问题。在多线程的设计中不只是要考虑访问数据的安全性,更要着重的考虑线程之间的通信问题。要从线程的设计方面,正确设计线程之间的有效通信,避免死锁问题。否则线程的不安全问题更是会带来一些不易复现的bug。再者就是要平衡好架构和性能的问题。好的架构和好的性能不是互相冲的方面,相反好的设计架构更容易构造出更优性能的程序。在多线程架构的设计中,一个架构清爽,安全性高的线程也更容易做出高性能程序。