BUAA-OO-Unit2 总结

BUAA-OO-Unit2 总结

1. 同步块与锁

1.1 锁的选择

​ 在本单元的三次作业中, 锁的选择均是使用synchronized关键字. 在课下我曾经尝试过读写锁的设置, 但经实际测试, 性能与synchronized相差不多, 甚至在部分测试点性能反而落后. 可能是由于在笔者的设计当中读写操作相间, 故而性能提升并不明显, 但反而由于读写锁的机制更加复杂导致时间性能反而落后, 并且jdk对synchronized关键字的优化已经使其性能与其它锁差不多.

1. 2 同步块的设置

​ 在第一、二次作业中, 由于我采用的架构中, 候乘表类既需要输入线程(生产者)来添加指令, 也需要多个电梯线程(消费者)来取走指令, 故而笔者将候乘表类封装为线程安全类, 这样生产者和消费者就可直接调用相关方法来完成职能. 故而在候乘表类中所有的对外方法我都使用synchronized对该对象进行了加锁操作.

​ 关于锁的粒度选择, 应尽可能地减小锁的粒度. 可以采取的方案是在每一次需要进行查操作时先加锁拷贝下来, 再遍历查询. 做到锁的极小化, 尽可能地不影响并发效率. 对于需要修改候乘表类中容器的方法, 则直接加锁锁住了整个方法.

​ 在第三次作业中, 由于电梯线程需要实时通过调度器更新下一阶段的请求到相应的候乘表中, 故而额外将调度器中update方法进行加锁.

2. 调度器设计

​ 在本单元架构设计时, 对于调度器的设计是比较纠结的, 纠结的问题就在于是否需要把调度器设计为单独的线程? 从功能的角度来讲, 调度器主要负责:

  • 各楼座指令的分发, 涉及到后续作业的迭代可能需要分析指令以达到换乘的目的;
  • 此外, 调度器还需要接收到输入结束的信号并且负责关闭各候乘表.

​ 思考再三, 笔者发现以上所说的过程不需要调度器主动地去完成, 而是可以被输入线程直接调用方法来完成职能, 并且由于不是共享对象, 也不需要加锁处理. 故而笔者没有选择将调度器设计为单独的线程. 客观来讲, 将调度器设计为线程可以达到解耦合的功用, 但却会增加维护线程运行和正常结束的复杂度.

3. 作业分析

3.1 第一次作业

3.1.1 UML图

​ 在本次作业中, 笔者主要采取的设计思想即为: 电梯只负责接收目标楼层, 然后按照内部逻辑来运行; 而负责给电梯分配目标的任务则全部交给策略类去实现, 因而策略类会管理着当前电梯和当前楼座候乘表的视图, 进而分析出电梯的下一目标, 这样设计可以有效地解耦合. 在策略方面, 我使用的是look算法, 其核心思想如下:

  • 在电梯中存在请求时, 实现捎带机制, 只沿路接与运行方向一致的请求. 保证电梯中所有请求方向一致, 这样就不用特意去遴选电梯中的主请求, 一个个送即可.
  • 在电梯中不存在请求时,
    • 在运行方向上选择最远的请求, 不论请求方向是否与运行方向一致; 确保不会漏掉高层/低层的请求而匆忙转向, 导致后面需要花大量的移动时间来接人.
    • 在运行方向上确认无请求后, 选择转向. 重复上述步骤.

​ 而电梯的运行逻辑则采用有限状态机来实现, 在我的设计中共有如下三种状态: running/waiting/opening, 分别对应着运行、静等和开门接送人三个状态. 值得一提的是, 通过阅读了学长们的博客, 发现为了进一步解耦合, 可将Status设置为内部类, 这样既可避免电梯的更多数据暴露出去.

  • 架构扩展能力

​ 在本次设计中, 由于并没有预想到后续作业会出现横向电梯(x), 因此只具象实现了纵向电梯类, 因此此架构之于第二次作业要求下的可扩展性并不好.

3.1.2 时序图

3.1.3 bug分析

  • 自己程序的bug

​ 笔者在本次作业的强测和互测中均未被发现bug.

​ 但在中测时出现了bug, 原因是因为在电梯线程结束判断条件有误, 导致在某些情况下可能会出现候乘表中仍有乘客, 而电梯线程就会提前结束的境况. 正确的电梯线程逻辑即是输入结束 && 对应候乘表空 && 电梯内部队列空.

  • 他人程序的bug

​ 在本次作业中, 房内其它四人出现了同样的问题, 就是输出不线程安全.

3.1.4 测试策略

​ 在自测时, 笔者实现了评测机, 由于多线程跑点只跑对一次并不代表没有bug, 因而在本地自测时, 采取多路并发一起跑同一个点的策略;数据生成方面, 分别从以下几个角度切入生成数据:

  • 输入时间密集型
  • 输入时间分散型
  • 输入楼座分散型
  • 输入楼座密集型

​ 除此之外, 还额外增加了1和10楼的出现概率, 以进一步测试电梯的运行逻辑. 对于RTLE, 笔者通过与同学对拍的方式进行测试; 而CTLE, 笔者是通过在ubuntu20.04环境下利用time进行测试.

3.2 第二次作业

3.2.1 UML图

本次作业笔者经历了重构. 在拿到本次作业指导书后, 笔者发现横向电梯和纵向电梯在行为上具有高度的一致性, 二者的差异性主要在于移动的"横竖二象性", 于是在本次作业中将电梯主体抽象出来, 在保留其内部运行逻辑不变以及状态转移逻辑不变的前提下, 由子类去实现具体的移动方法, 这样即可提高程序的可扩展性, 增加代码复用率.

​ 对于候乘表类, 笔者采取了同样的抽象策略, 将候乘表类抽象, 具体的某些操作交给子类去实现, 即可实现横向电梯和纵向电梯不同的检索策略.

  • 架构扩展能力

​ 在本次作业中, 将横向电梯和纵向电梯进行抽象, 抽象出电梯具体的运行逻辑, 并将具体的移动方法交给子类去实现. 若后续增加其它类型的电梯, 直接覆写电梯子类的移动方法即可正常工作.

​ 由于已经预判到第三次作业的换乘要求, 故而留出了一些必要的接口以供第三次作业扩展.

3.2.2 时序图

3.2.3 bug分析

  • 自己程序的bug.

​ 笔者在本次作业的公测和互测中均未被发现bug.

  • 他人程序的bug

​ 在互测时, 发现房内有同学由于同楼座的多个电梯之间共享侯乘队列, 并且在接送人时未对更改了侯乘队列且未加锁, 因而会导致抛出异常. 但在用一组数据反复提交的时候, 误伤到了另外一位同学, 不知道是什么原因...(在这里表示对不起qaq)

3.2.4 测试策略

​ 在本地测试时, 与ysy同学合作完成评测机, 他负责数据生成方面, 笔者主要负责编写spj和自动评测方面. 在编写spj时, 由于有了第一次作业编写spj的经验, 发现纯用面向过程的思路来写spj不仅代码可读性差, 容易漏掉相关判据, 并且还不具有可扩展性. 于是本次作业通过OOP来模拟整个过程, 并为下一次作业的换乘和特种电梯留下了可扩展的空间.

​ 在自动评测方面, 还是选择多路并发的方式进行测试.

3.3 第三次作业

​ 在本次作业中增加换乘的请求, 笔者在此只考虑了必须换乘的请求, 未考虑为了减小等待时间而拆分不必要换乘的请求. 通过指导书分析可知, 本次作业比较关键的有两点: 换乘策略和结束判断.

​ 关于换乘策略, 笔者采用动态维护所有可换乘点的加权图, 权重即为能走通此条路的所有电梯的平均速度(为了呼应自由竞争的策略), 并为每个换乘点添加换乘惩罚来尽可能减小换乘的次数, 通过Dijkstra算法静态构建出换乘请求的所有阶段, 并通过链表进行每一阶段的维护. 当然, 这种做法也有不科学的地方, 也就是由于电梯的种类组合过多, 无法考虑到具体某个电梯的状态(包括请求到来时的位置, 方向以及电梯上的人数), 并且由于笔者在hw6中采取自由竞争的策略, 路径的规划显得更加难以实现, 此处给出一种可能的处理方案, 将上述因素通过权重函数并求平均值的思路. 但限于时间和精力原因, 笔者并未在本次作业中实现.

​ 对于结束判断, 笔者采用另设计数器类, 来对此时未完成的请求进行计数, 若输入结束 && 计数器为0, 则结束所有的电梯线程, 需要注意的是, 由于计数器类是所有电梯线程的共享对象, 此处需要对其额外加锁或将其封装成AtomicInteger原子类进行加减操作.

3.3.1 UML图

​ 相比于上次作业, 添加了Analyzer类和ElevatorManager类, 其分别负责分析换成请求给出最终路线, 和电梯管理的功用. 可以看到, 其它类在大体结构上并未发生变化.

  • 架构扩展能力

​ 关于换乘依据的选择上, 选择了最短路来规划路径, 笔者认为这种路径规划方法具有一定的扩展性. 而课程组给的基准策略仅仅是建立在初始就具有一台全楼座可停靠的横向电梯的条件下正确性才可保证, 若去掉此条件, 就无法通过基准策略进行路径规划. 此时, 则需要依据最短路来进行规划.

3.3.2 时序图

3.3.3 bug分析

  • 自己程序的bug

​ 笔者在本次作业的公测和互测中均未被发现bug. 但在本地自测时, 出现了一些bug...

​ 首先是轮询的问题, 由于本次特种电梯的引入, 导致判断电梯是否应该wait的条件发生了变化, 也就是变为对应候乘表中无自己能接的乘客就阻塞. 并且, 在候乘表中实现的检索方法, 也应将电梯的可停靠性纳入考虑, 否则会导致电梯的状态一直在opening反复横跳却接不到人....CPU使用时间直接到了20s...出现这一bug的根本原因在于拿到指导书后, 并未分析自己已有程序的各个模块的方法条件与迭代的内容的区别之处.

​ 其次是结束判断的问题, 在一开始笔者采用判断当电梯进入到wait状态时, 就先判断其余所有电梯是否全部处于空等状态, 若是且输入结束, 则发出终止信号. 但此时电梯的状态也就成为了所有电梯的共享对象, 笔者起初并未意识到这个问题, 导致偶尔会出现最后一个人的最后一段请求送不到的情况. 后更改为计数器判断结束法.

  • 他人程序的bug

​ 在本次互测中, 笔者并未投入大量精力, 仅仅是将房内其余几人的程序打成jar包放入评测机, 最后批量提交错误样例. 有一位同学的bug在于无法正常结束. 另外一位同学则是会出现无法送达所有请求的bug.

3.3.4 测试策略

​ 关于本次作业的自测, 与上次作业保持一致, 依然采取和同学合作的方案. 只不过为了呼应互测时80路并发测试, 本地测试时增加了测试线程个数.

4. 关于测试

​ 在本单元中, 笔者主要采用黑盒测试的方式, 此外还会通过每次作业画状态转移图来分析电梯内部运行逻辑的正确性.

4.1 与第一单元测试策略的差异

​ 笔者认为, 本单元测试与第一单元最大的差异有如下几点:

  • 测试的效率难以避免地降低. 如何提高测试效率, 可以尝试subprocess模块.
  • 由于测试量的减小, 更加要求生成数据的质量, 需要做到数据生成逻辑自有方向, 不能试图纯靠随机去碰运气.
  • 需要思考如何保证测试的准确, spj的判据种类繁多, 一定要做到不遗漏. 同时, 跑点一次正确并不代表没有问题, 只有模拟多次才可保证自己的程序在此数据点上不会出现bug.
  • 由于多线程debug的难以复现性, 需要在自动化测试时额外考虑如何保存错误的数据点、错误的输出包括错误的原因.
  • 除此之外, 还需在测试时囊括对处理器使用时间的考虑, 避免CTLE.

5. 心得体会

5.1 线程安全

​ 在多线程开发实践中, 最考究的问题就是线程安全, 为了避免此问题, 首先应该在设计之初考虑以下几个问题:

  • 在本项目中需要实现几个线程? 各自有怎样的功用? 需要管理的数据都有哪些?
  • 分析哪些属于临界资源.

​ 其次, 对于轮询的理解也进一步加深, 也就是在某些线程没必要工作的时候, 避免其忙等而浪费CPU资源. 为了避免轮询, 需要考虑清楚的问题也就是这些线程什么时候才有必要工作? 这个时机是什么?

​ 死锁的问题也不能忽略. 在本单元作业中, 笔者尽可能地避免循环上锁的情况, 尽可能让线程每次只拿一把锁. 破除循环等待条件, 即每个线程一次至多取得一把锁, 依此方案可避免死锁问题.

​ 此外, 还学习并应用了许多设计模式, 例如单例模式、状态模式、流水线模式和策略模式, 应用这些设计模式, 不仅可使代码可读性增加, 还进一步降低了类之间的耦合, 增加了可维护性和可扩展性. 如何设计类之间的职能和交互, 应该才是这门课最需要培养的能力.

​ 当然, 还会有些遗憾的地方, 笔者并没有采用工程界常常使用的可重入锁, 其比synchronized更为灵活; 在设计模式的应用上, 在第一次作业后并未有新的尝试和突破......

5.2 层次化设计

​ 在本单元第二次作业中, 在迭代添加了横向电梯后, 笔者在发现横竖电梯行为及运行逻辑上的高度一致性后, 及时选择重构, 将电梯主体抽象出来, 形成层次化结构, 不仅可以提高可维护性, 可扩展性也相应提高, 符合OOP层次化设计的理念.

posted @ 2022-05-03 08:55  Wang_zm  阅读(61)  评论(0编辑  收藏  举报