第二单元总结——电梯调度

第二单元总结

作业的大体架构

  • 以第三次作业为模板
graph TD A["读入线程"] --> B B["等待队列(tray)"] --> C C["一级调度器"] --> D C --> E D["楼层(tray,10个)"] --> |每个楼层有自己的调度器| F E["楼座(tray,5个)"] --> |每个楼座有自己的调度器| G F["二级调度器(floor)"] --> |每个电梯有自己的队列| H G["二级调度器(building)"] --> |每个电梯有自己的队列|I H["楼座电梯请求队列(tray)"] --> J I["楼层电梯请求队列(tray)"] --> K J["横向电梯线程(可添加)"] --> |未到达目的地 回写| B K["纵向电梯线程(可添加)"] --> |未到达目的地 回写| B

关于线程锁设计

  • 课程中提到了多种上锁的方式
    • 使用 synchronized关键字,可以修饰普通方法、静态方法,以及语句块
    • 使用ReenreantLock类,通过类方法实现锁,和synchronized类似
    • 使用原子变量实现线程同步(原子操作就是指将读取变量值、修改变量值、保存变量值看成一个整体来操作,即-这几种行为要么同时完成,要么都不完成)
  • 但是由于最开始只提到了 synchronized关键字(关键是十分方便),所以本单元的作业的同步锁全部使用了该方法
  • 什么时候需要上锁?
    • 其实计算机语言作为一个人类设计出来的东西,所有的操作都应该的理所当然的
    • 作为一个人,我们无法在同一时间与两个或者两个以上的人交流(如果你认为可以的话,只是因为你的大脑切换的做够快);两个人无法同时在纸上的同一个地方写字,当然也不能别人还没把话说完就去臆测别人的用意
    • 那么电脑的运行逻辑应该和我们一样
    • 所以同一个内存不可以被两个人同时修改,不能在修改的同时读取,于是我们设计锁的逻辑就是给所有有可能被两个人(在程序中就是线程了)同时访问的对象上个锁,同一时间只有一个人可以修改或读取这些内容
  • 在本次作业中需要上锁的地方(以第三次作业为例)
    • 其实画出架构图之后什么地方上锁就一目了然了
    • 我的架构中读入、调度器、电梯都是线程
    • 所有夹在多个线程之间的类都需要对类内部的某些属性加锁,甚至给整个类上锁
    • 在我的架构中所有的托盘都只维护了一个队列,所以直接给整个类上锁就可以简单的实现目的
    • 然后我们把这些需要加锁的用来存数据的类称为共享对象
  • 有没有不需要上锁的东西呢?
    • 在我的电梯类型中维护了一个乘客队列来保存电梯内的乘客
    • 这个队列就不需要上锁
    • 因为它只被电梯本身这一个线程访问

关于调度器的设计

  • 在我看来调度器的设计无非是在单个调度器的复杂度和调度器层次结构的复杂度上做平衡
    • 减少层次必然会使调度器更加复杂,这会使代码变得复杂,容易出现逻辑错误
    • 而减小调度器的复杂度也必然会使结构层次变复杂,这会使debug变得复杂
  • 我的作业规划为三级调度(电梯也算一级)
    • 第一级负责进行楼层和楼座的调度
      • 这一级调度器决定了乘客的行进方向
      • 如果是纵向行进,则也同时决定乘客去往哪一层
      • 设置乘客目的地后,调度器将乘客分配给特定的楼层、楼座执行运输任务
    • 第二级调度器进行楼层、楼座内的电梯分配
      • 调度器会将乘客分配给一个调度器认为合适的电梯的请求队列
      • 这个“合适”主要取决于调度策略
    • 第三级调度器也就是电梯将乘客送往本次行进的目的地
      • 电梯根据一级调度器设置的目的地将乘客送往指定的地方
      • 如果乘客没有到达最终的目的地,会被再次送入开头进行再次调度
  • 关于交互方式
    • 数据交互
      • 托盘提供add和get方法以供调度器获取请求
      • 调度器在合适的时候调用托盘的方法获取请求并处理
    • 结束程序
      • 上层的调度器会在自任务结束时通知下一层托盘,并结束当前线程
      • 以此类推,层层通知,最终整个程序结束
    • 总结
      • 本次作业使用了多种交互模式,是消费者模式、流水线模式、观察者模式、单例模式(流水线的回写方式仿照课上实验,使用了单例模式)的集合体

作业的分析与总结

  • 第三次作业类图

  • 协作图

    sequenceDiagram participant main participant ReqInput participant waitQueue participant Shedule participant Building/Floor participant B/FShedule participant B/F EReqQueue participant Elevator Note left of main: 创建所有初始<br/>内容和后结束 loop 读取 ReqInput -> ReqInput: 不停的读取数据知道结束 end ReqInput->>waitQueue: 读取请求<br/>交给托盘<br/>所有的请求处理完后<br/>通知结束 waitQueue->>Shedule: Shedule调用<br/>getOneReq方法时<br/>将一个请求交给调度器 loop 调度 Shedule->Shedule: 将获取的数据调度到<br/>对应的楼层或楼座 end Shedule->>Building/Floor: 调度请求<br/>交给托盘<br/>所有的请求处理完后<br/>通知结束 Building/Floor->>B/FShedule: B/FShedule调用<br/>getOneReq方法时<br/>将一个请求交给调度器 loop 调度 B/FShedule->B/FShedule: 将获取的数据调度到<br/>合适的电梯请求队列中 end B/FShedule->B/F EReqQueue: 调度请求<br/>交给托盘<br/>所有的请求处理完后<br/>通知结束 B/F EReqQueue->Elevator: Elevator调用<br/>passengerGetUp时<br/>将所有符合条件<br/>的乘客交给电梯 loop 运输 Elevator->Elevator: 运输乘客到指定位置<br/>没有乘客时wait end Elevator->>waitQueue: 如果乘客下电梯时还没有到达目的地<br/>则回写到流水线开头等待重新调度
  • 整体思路

    • 本单元作业的思路还是很清晰的,主要是课程中介绍了许多编程模式,按照需求使用不同的模式可以有效的提高编程的效率
    • 整体上是一个多级的生产者消费者模式;在线程结束方面使用了观察者模式;同时在电梯运输方面采取流水线模式与单例模式相结合的模式,通过分阶段处理解决换乘问题
  • 迭代

    • 在第一单元的作业结束后,我对代码的可扩展性有了一定的认识,本次作业中的可扩展性还不错

    • 比如说电梯类,纵向电梯类从第一次完成后就没有再次修改过。同时通过添加函数实现了横向电梯的look调度

      // 横向电梯通过这两个函数将楼层的逻辑移植到了楼座上
      private char numToBuilding(int num) {
          // 根据自身的楼座,将[-2,2]映射到楼座名称
          return (char)((building - 'A' + 5 + num) % 5 + 'A');
      }
      
      private int buildingToNum(char building) {
          // 根据自身的数字,将楼座名称映射到[-2,2]
          return (building + 2 - this.building) % 5 - 2;
      }
      
    • 总的来说,如果想要程序容易迭代,就需要准确把握每个类的功能,做到高内聚低耦合;以及尽量把所有可能发生改变的量设置为类的属性比如

      // 将电梯的各种参量如容量、速度等看起来不可变但事实上在初始化时可以设置的常量设置为属性
      public class FElevator implements Runnable, Elevator {
      	// 处理队列
          private final FReqQueue flReqQueue;
          private final int id;
          // key: toBuilding
          private final HashMap<Character, LinkedList<Passenger>> passengers;
          private char building;
          private final int floor;
          private int direction; // 1: left, -1: right, 0: stop
          private int lode;
          private final int maxLode;
          private final long speed;
          private final int switchInfo;
          private boolean isEnd;
          
          ...
      }
      
    • 可以看出第二次作业扩充的类比较多,主要是因为一二次作业改动比较大

    • 第三次作业类的扩展比较少,只是将托盘的相似代码做了整合,又调整了调度器的一些函数

  • 复杂度分析(针对第三次作业,只摘取了复杂度较高的函数)

    • method CogC ev(G) iv(G) v(G)
      BElevator.downReq() 12.0 6 3.0 6.0
      BElevator.upReq() 12.0 6 3.0 6.0
      BSchedule.run() 12.0 4 6.0 7.0
      FSchedule.run() 12.0 4 6.0 7.0
      Schedule.run() 12.0 4 6.0 7.0
      BReqQueue.getReq(int, int, int) 16.0 3 8.0 9.0
      FReqQueue.getReq(char, int, int) 16.0 3 8.0 9.0
      FElevator.renewDirection() 38.0 14.0 10.0 14.0
    • 本次作业的复杂函数非常少主要是由于对一些功能复杂的函数进行了拆分

    • 比如判断电梯的run函数:

      • public void run() {
            while (!isEnd) {
                renewDirection();
                if (needStop()) {
                    openTheDoor();
                    passengerGetOff();
                    passengerGetUp();
                    closeTheDoor();
                }
                renewDirection();
                if (direction == 1) {
                    moveUp();
                } else if (direction == -1) {
                    moveDown();
                } else {
                    bdReqQueue.elevatorWaitOrStop(this);
                }
            }
        }
        // 共调用9个函数
        // 本函数中的函数除了会调用本类的一些函数,还会调用托盘的一些判断函数
        // 总之,本函数的实现依赖了多个对象的函数的相互协作
        

bug分析

  • 第一次作业

    • 没有对输出方法进行线程安全处理,导致时间戳不单调递增,属于典型的线程安全问题
    • 电梯的方向更新策略在特定情况下会使电梯在两层楼之间反复横跳,属于逻辑问题
  • 第二次作业

    • 乘客的横向方向判定没有进行修改(电梯里的改了,乘客的忘了,哭)

      // 修改前
      public boolean isMoveLeft() {
      	return disBuildig - fromBuilding <= 2;
      }
      // 修改后
      public boolean isMoveLeft() {
      	return (disBuilding + 2 - fromBuilding) % 5 - 2 > 0;
      }
      

      吸取教训,应该把方向判定的函数写在一个工具类里,毕竟很多类都用到了,逐一修改会很麻烦

  • 第三次作业

    • 没有被hack出bug,诶嘿

hack策略

  • 关于策略
    • 其实对于多线程的问题没有想出太好的hack策略,或许有的时候没有策略就是最好的策略,只要测试数据够大概率会有bug
    • 同时也针对几个点进行了数据构造
      • 轮询(竟然真有人这么干):在突发的喂入一定的数据后,等待个几十秒,再喂一点点数据
      • 策略时间较长:在69秒,喂入最大数量的数据(数据尽量集中在一两部电梯,多跨楼层)
  • 关于hack线程安全
    • 至于发现线程安全的问题主要还是读代码以及随机测试
    • 主要是因为这个问题比较玄学,一个又线程安全问题的程序在一次测试中可能完全没有出问题,比如我的第一次作业,强测都没有hack出线程安全问题,但实际上是存在问题的
    • 因此好像也没有什么好的办法,大量测试吧
  • 与第一单元测试策略的差异之处
    • 第一次的测试感觉比较有测试的方向,可以针对边界,以及偏怪数据进行测试,主要还是对边界条件的测试居多
    • 但是第二次作业给人无从下手的感觉,甚至感觉随机测试就是最好的方法,可针对的边界条件几乎没有,而由于线程运行造成的bug又是很难通过读代码发现的

心得体会

  • 总的来说本次作业比第一单元轻松了不少,多线程的问题和普通的程序相比还是有很大的区别,尤其是debug太痛苦了。比较令我耳目一新的是java处理同步的方式,既可以通过关键字,又可以通过给定的锁类自定义。
  • 多线程的学习更多的还是学习到了很多新的编程模式消费者模式、流水线模式等等,不得不说没有成熟的方法学指导,是很难做成一件事的
posted @ 2022-04-29 22:36  小黑要努力呀  阅读(27)  评论(0编辑  收藏  举报