BUAA_OO_第二单元作业总结_电梯

BUAA_OO_第二单元作业总结_电梯

1.第一次作业

  1. 同步块设计和锁的选择

    1. 本次作业分别进行了两次同步块设计,最终采用三个线程设计

      1. 两个线程的设计:
        1. 根据本次作业要求,很自然想到生产者、消费者模式设计
          • 生产者:输入线程
          • 消费者:电梯线程
          • 托盘:调度器(其实就第一次作业来说,该调度器作用仅起到等待电梯调度的指令序列存储作用,无实际操作)
        2. 该设计可扩展性不高,且较为简洁,不再赘述。
      2. 三个线程的设计:
        1. 受到上机实验的启发,为提高电梯的可扩展性(对于本次实验来说,没有实际意义),采用了三个线程的设计(输入线程,调度器线程,电梯线程),设置了共享对象的类:等待被调度器分配给电梯的指令队列类(waitQueue),等待被某一个电梯处理的指令队列类(elevatorQueue)
          • 生产者1:输入线程
          • 托盘1:等待被调度器分配给电梯的指令队列(waitQueue)
          • 消费者1:调度器线程
          • 生产者2:调度器线程
          • 托盘2:等待被某一个电梯处理的指令队列(elevatorQueue,表示在不同楼层等待电梯接送的人的队列)
          • 消费者2:电梯线程
        2. 生产者1-托盘1-消费者1:
          • 托盘1(waitQueue)作为了共享对象,所以将其设计为线程安全类(具体见后文线程安全设计),输入线程不断读入乘客指令,将其存在waitQueue中等待被消费者(调度器线程)调度取出,进行后述操作。
        3. 生产者2-托盘2-消费者2:
          • 托盘2(elevatorQueue)作为共享对象,保证其线程安全性,进行前述操作后,调度器线程将取出的乘客指令存在托盘(等待被电梯处理的队列)中,等待电梯线程将其取出,放在电梯类内部的passengers(表示在电梯内部的人等待被送达的人的队列)中,电梯线程根据内部的调度接人算法取出托盘中不同楼层的指令。
    2. 锁的选择和线程安全的设计

      1. 本单元的作业整体的锁的设计基本类似,受到了讨论区的买月饼的设计的启发,我们本次实验主要使用的是生产者-消费者模式,可能出现线程安全问题的仅仅是我们的共享对象——托盘,因此考虑将线程安全控制集成到托盘类中,实现线程安全类。
      2. 因此本次实验主要通过将业务逻辑都抽取在线程安全类的方法中,使用synchronized将对共享对象操作的方法对该共享对象进行加锁,保证了在调用线程安全类内部方法时只能一个线程拿到锁进行调用,实现线程安全。
  2. 调度器设计

    1. 本次作业的总调度器并不进行相关的调度算法操作,只是一股脑将所有的托盘1(waitqueue)中的指令取出放在托盘2(elevatorqueue)中,交互过程见前面同步块设计分析。
    2. 电梯内部的调度算法采用了look算法:
      1. 电梯在向某一个方向运行时,若电梯所在层有人等待希望前往的方向和电梯运行方向相同,则判断该电梯是否已经满载,然后将该人捎带。
      2. 电梯内部有乘客,则优先处理电梯内部队列,即根据电梯内部队列乘客的请求确定运行方向,即电梯内有乘客和当前运行方向相同,则电梯不转向,否则电梯转向。
      3. 若电梯内部为空,且在电梯运行的方向上的楼层没有乘客在等待,则将电梯转向。
  3. 程序bug分析

    第一次作业在公测和互测中没有出现bug。

    由于采用了线程安全类的做法,因此在对共享对象操作时没有出现死锁等线程安全问题。

2.第二次作业

  1. 同步块设计和锁的选择

    1. 同步块设计

      1. 本次作业的同步块设计在第一次作业的三线程基础上没有做过多的改动,但由于增加了多部电梯,因此增加了电梯线程的数量,调度器线程和电梯线程之间的托盘数量,用一个ArrayList来存放每一个电梯线程对应的托盘(elevatorQueue),其大致的结构为:

    2. 锁的选择和线程安全的设计

      继续沿用上次作业的锁的设计和线程安全的设计,但是新增了电梯状态的共享对象类,因此需要效仿之前对托盘类进行的线程安全处理方法来将电梯状态类(elevatorStatus)处理为线程安全类。

  2. 调度器设计

    1. 由于本次作业新添了增加电梯的指令,因此需要在调度器中,对指令类型进行判断,如果是增加电梯的指令,则需要实例化一个elevatorQueue,将其加入elevatorQueues中,并新new一个elevator的进程,从而达到增加电梯的目的。

    2. 对于乘客指令,与第一次作业相比,需要对于不同的乘客指令结合调度算法进行调度进入不同的电梯等待队列。

    3. 由于调度时,需要获取电梯的状态,因此新增了电梯状态类(elevatorStatus)作为电梯线程和调度器线程的托盘,即:

      • 生产者:电梯线程
      • 托盘:电梯的状态参数(电梯内部乘客数,电梯当前楼层等)
      • 消费者:调度器线程

      电梯每一次进人,下人,移动都需要更新elevatorStatus类的参数,调度器则取出每一个电梯的参数,结合设计的调度算法和当前需要被调度的乘客指令,计算出每一个电梯的权重,最后经过比较,将该乘客指令存在权重较小的电梯对应的elevatorQueue中。

    4. 电梯调度权重计算主要以等待电梯调度队列的人数,电梯内部人数,电梯等待队列里出发地、方向和该指令相同的人数,电梯和该乘客之间的里程,给不同参数不同的权重,最终算出总权重用于前述的比较。(经测试发现将电梯与该乘客之间的里程数作为参数的效果并不好,最终没有将该参数考虑进去)。

  3. 程序bug分析

    第二次作业在强测超时了一个数据点,主要是该数据点考虑的是考察morning模式的捎带,而由于我之前的疏忽,遍历的时候用了另一种写法,第一次电梯只能捎带一个人,后面才可以一次捎带6个人,导致了这个180秒才开始进人的数据点运行超过30s,最终导致了超时。

    没有出现线程安全问题。

3.第三次作业

  1. 同步块设计和锁的选择

    本次作业的同步块设计和锁的选择和前一次作业基本类似,不再赘述。

  2. 调度器设计

    本次作业新加了换乘的需求,最终完成了两版的内容,分别为进行换乘,和只是单纯地将能被特定电梯运载的乘客放到对应的特定电梯里,剩余的全部给A电梯,但最终经过测试发现,后者不换乘的做法在本次作业中并不会比换乘慢多少,反而在一些随机数据中比换乘运行更快,因此最后采用的不换乘的策略,同种电梯继续沿用前一次作业的调度算法。

    在这里简略地介绍一下第一版换乘的调度器实现,为满足换成的需求,新建了person类,并赋予person类是否换乘,换乘楼层,目标楼层,起始楼层的属性,调度器每从waitQueue中取一条指令,判断该乘客是否需要换乘,并新new一个person类结合调度算法添加对应的属性,最终放入elevatorQueue中。

    换乘的实质就是消费者在工作一半后,处理完指令到达换乘楼层,新产生一个person类型的不需要换乘的指令,交给调度器重新结合调度算法调度给对应的电梯,对于如何将指令重新放回调度器进行调度,仍旧采用生产者消费者模型,新建托盘类(TransferQueue),存放待重新调度的指令,电梯为生产者,调度器为消费者。调度器内部不光要调度waitqueue中的指令,还要对每一个电梯的TransferQueue进行取指令和调度。

  3. 程序bug分析

    第三次作业在公测和互测中没有出现bug。

    针对第一版换乘内容,对于结束电梯线程课下调试时,发现不能继续沿用前面的结束判断条件,而要根据每一个电梯的状态,只有当所有电梯中都没有待处理的乘客,调度器再下发结束信号,因为可能电梯内部乘客指令可能新产生指令。

4.分析和总结第三次作业架构设计的可扩展性

4.1.UML类图

  • 第一次作业

  • 第二次作业

  • 第三次作业

4.2.UML协作图

  • 第一次作业

  • 第二次作业和第三次作业(两次作业的协作图基本类似因此一起进行分析)

4.3.第三次作业可扩展性

本次作业基本满足了面向对象的设计思想,各司其职,比如电梯仅负责处理调度器分配给他的指令,调度器仅负责调度输入线程和电梯线程产生的指令,想要增加新的调度方法,只需要在调度器中处理调度算法,想要增加电梯的功能和限定,只需要在电梯内部改变其属性,而不用改动过多的类和方法,而第三次作业和第二次作业其实也仅仅就是在第一次作业的框架上增加了新的调度算法,和新的盘子,并没有太大的代码增量,因此可扩展性良好。

5.hack他人程序bug策略

  • 自己所采取的测试策略及有效性

    本单元作业的测试依旧采用黑盒测试,随机生成数据,进行检查对拍,但是这种方法的效果并不显著,一是随机产生的数据本身强度不高,二是本次多线程作业运行时间本身较长,同时测试多份代码耗时较多,因此测试的随机数据也不够充分,因此最终也没有hack成功。

  • 发现线程安全相关的问题策略

    本单元分析线程安全策略起初为通过阅读他人代码,检查锁的设置是否合理,但是经常阅读了几份代码就懒得继续浏览下去,因此主要还是通过随机大量数据,通过评测机进行自动测试,不过并未发现他人线程安全问题bug。

  • 分析本单元的测试策略与第一单元测试策略的差异之处

    本单元测试策略依旧采用第一单元的黑盒测试,但是第一单元可以自己通过思考构造一些特殊数据,来测试一些特殊情况,但是本单元作业,特殊情况较少,通过人脑构造特殊数据有效性不高,并且如果没有出现线程安全问题较难卡T。

    并且本次单元作业最大特点是bug难以复现,可能找到一个bug,需要重复提交多次同一个数据点才可能成功复现,这也可能是造成本次作业互测屋hack得并不激烈的原因。

6.心得体会

本单元作业初窥多线程编程:

  • 线程安全问题:
    • 从讨论区的买月饼教程中学习到了让“桌子”变得更高级的做法,本次作业主要都是通过在共享对象的类中,对方法使用的共享对象加锁,因此出现的锁主要集中在了托盘类中,并不会出现锁到处都是的问题,简化了代码的锁的逻辑。
    • 对于临界区的划分也是线程安全方面主要思考的问题,哪些操作需要同时加锁保证操作的原子性,哪些操作不能加锁,这些都是加锁的艺术。
  • 多线程调试和修改bug:在本单元调试过程中,体会到了bug无法复现的痛苦,而这种问题也很难通过调试来进行解决,只能一遍一遍地阅读代码,检查锁的设置,并且由于多线程的难以复现,对于一些bug也不知道修改后是否成功,只能每次修改随机大量数据来检查。
  • 层次化设计:本次的层次化设计让我对面向对象设计有了进一步的理解,正如老师上课说的C语言程序一个main下来的程序结构是很不好的,一个好的程序结构一定要进行优良的程序结构层次化划分,比如在本次就是划分为了三个层次,一个输入层次,一个调度器处理层次,和一个电梯执行输出层次,从而使得程序的耦合度大大降低,可扩展性变得更好。

不过本次作业不用再像上一个单元面向输入,费尽心思构造正则表达式,使得我能花费更多精力在多线程的学习,面向对象的思考和构造架构上。

总的来说,体验良好,本单元作业不光是多线程的学习,电梯调度的算法的思考,还是费尽心思地debug都让我体会到了本单元作业的“有趣”的特色,并且本单元作业也提供了一个实践面向对象思想和多线程入门的一个很好的锻炼机会。

posted @ 2021-04-24 15:51  做个废柴呐  阅读(61)  评论(1编辑  收藏  举报