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

BUAA_OO 第二单元总结

一. 程序架构分析

1. 第一次作业

  • 需求摘要
    本次作业,需要完成的任务为单部多线程可捎带电梯的模拟。

  • 设计策略
    主要采用生产者——消费者模式

    • 生产者:MyInput(输入线程)
    • 托盘:Dispatcher(调度器)
    • 消费者:Elevator(电梯线程)
    • Dispatcher(调度器)做为共享对象,接收来自输入线程发送的任务,之后将任务分配给电梯线程,为保证线程安全,使用synchronized关键字修饰addTask(Task)getTask()方法。
    • MyInput(输入线程)采用堵塞式读取的方式,调用addTask(Task)方法将有效任务加入Dispatcher中,输入结束后自动结束线程。
    • Elevator(电梯线程)调用getTask()方法获取指定任务,运送方式主要采用LOOK算法,即以离当前楼层最远的任务做为主任务,途中可接人或者放人。
  • 代码分析

    • UML类图
      主要有五个类:MainClass Dispatcher MyInput Elevator Task

    • 时序图
      MainCLass中启动MyInputElevator线程,每当有新任务时,MyInput线程将任务加入Dispatcher队列中并notifyAll() Elevator线程,Elevator将任务从Dispatcher中取出放入自己的队列,并开始执行任务,任务执行完毕进入wait()状态。当输入结束,MyInput线程发送一个特殊任务(id:0 from:0 to:0)并结束,Elevator线程接到并识别此任务为结束信号,完成所有有效任务后立即结束。

    • 复杂度分析
      类复杂度

      方法复杂度

      在此次的作业中,Elevator类的复杂度较高,主要体现在run()getInOff()两个方法上。

      run()方法因为要对MyInput输入状态以及Dispatcher中的队列进行判断导致if-elsewhile结构较多,过于复杂。

      getInOff()方法用于判断是否有人要在当前楼层下电梯或上电梯,主要对两个队列进行循环,同时也有较多if-else结构,导致此方法比较复杂(一种解决方案是将此函数分成getIn()getOff()两个函数,虽然在一定程度上可降低复杂度,但这两个函数的结构和功能都很相似,在调用时反而会很繁琐,所以此方案并未被采用)。

2. 第二次作业

  • 需求摘要
    本次作业,需要完成的任务为多部多线程可捎带调度电梯的模拟。

  • 设计策略
    和第一次作业基本相同,主要区别就是在MainClass类中先获取电梯的数量并启动多个电梯线程以及在Dispatcher中增加了多个队列(每个电梯对应一个队列)。
    算法采用均摊+找最近的方法(根据强测性能来分析这种方法比第一次LOOK算法表现要好)。

  • 代码分析

    • UML类图
      主要有五个类:MainClass Dispatcher MyInput Elevator Task

    • 时序图
      MainCLass中启动MyInput和多个Elevator线程,每当有新任务时,MyInput线程将任务加入Dispatcher相应队列中并notifyAll()所有Elevator线程,若Elevator相应队列不为空则将任务从Dispatcher中取出放入自己的队列,并开始执行任务,任务执行完毕进入wait()状态。当输入结束,MyInput线程向所有Elevator线程发送一个特殊任务(id:0 from:0 to:0)并结束,Elevator线程接到并识别此任务为结束信号,完成所有有效任务后立即结束。

    • 复杂度分析
      类复杂度

      方法复杂度

      此次的作业是在第一次作业拓展而来的,并未涉及重构(主要修改一下类的属性),所以类复杂度和方法复杂度基本相似,在此不再赘述。

3. 第三次作业

  • 需求摘要
    本次作业,需要完成的任务为多部多线程可捎带调度电梯的模拟。

  • 设计策略
    采用生产者——消费者模式,模式和类的对应与前两次作业相同。
    算法采用均摊+找最近的方法(因第二次表现较好所以继续采用,但实际上根据第三次强测的性能来看表现不是很理想)。
    和前两次作业不同的是,这次电梯有三种类型,每个类型能停靠的楼层、最大载人数、运行时间都不同,而且还可以中途加入不同类型的电梯,所以策略如下

    • 能不换乘就不换乘
    • 必须要换乘则能换乘一次就不换乘两次(因为有1层和15层的存在,所以没有必须要换乘两次的任务)
    • 根据电梯运行状态的不同选择合适的换乘楼层(不设置固定的换乘楼层)
    • 为保证线程安全,增加安全输出类SafeOutPut
    • 降低数据冗余,增加数据类Data
  • 代码分析

    • UML类图
      主要有七个类:MainClass Dispatcher MyInput Elevator Task SafeOutPut Data

    • 时序图
      MainCLass中启动MyInput和三个初始的Elevator线程。每当有新任务时,MyInput线程将任务加入Dispatcher相应队列中并notifyAll()所有Elevator线程,若Elevator相应队列不为空则将任务从Dispatcher中取出放入自己的队列,并开始执行任务,若为换乘任务则在完成后将任务再次加入Dispatcher相应队列,所有任务执行完毕进入wait()状态;每当有新电梯加入时,MyInput线程将完成相应信息的更新并启动新线程;输入结束时,MyInput线程向所有Elevator线程发送一个特殊任务(id:0 from:0 to:0)并结束,Elevator线程接到并识别此任务为结束信号,当所有电梯完成所有有效任务后立即结束。

    • 复杂度分析
      类复杂度

      方法复杂度

      此次作业是在前一次作业基础上扩展而来,Elevator类复杂度有一些增加,而Dispatcher类复杂度增加了很多。
      Elevator类随着功能的增加复杂度也越来越大,主要原因还是上述提到了run()getInOff()两个方法过于复杂。
      为了线程的安全性和简易性此次作业未安排两个调度器,只有一个调度器负责接收来自输入线程和电梯线程两方面的任务,所以类复杂度相较于前两次提升了很多。主要体现在分配任务的函数上,例如addTask() assignTask()等。

  • 可扩展性

    • 功能分析
      功能正确性主要体现在Elevator类,在三次的作业中此类的内聚程度很高,只要接收到任务就能正确的完成,不需要获取其他外部的状态,如之后还有新功能,只需在Elevator类内部调整即可。
    • 性能分析
      在保证性能正确的基础上可以进一步追求性能,在这次的作业中提升性能的方式主要有两种
      • 调度器合理地分配任务
      • 电梯高效地执行任务
        分配任务采用调度器主动分配加上各电梯均摊的方式,执行任务采用找最近的方式。如果要进一步提高性能也只需要修改相关函数即可,比较容易实现。
  • SOLID原则

    • 单一职责原则
      • 电梯只负责完成接收到的任务
      • 调度器只负责接收和分配任务
      • 输入线程只负责发送新任务
      • 主类只负责启动初始线程
    • 开放封闭原则
      • 电梯类主要沿用第一次作业中的电梯,只小范围修改过属性
      • 调度器类改动较大,这次的作业调度器要从两方面接收任务,增加了一些方法
      • 输入类基本无变动
      • 主类基本无变动
    • 里氏替换原则:未涉及类之间的继承关系
    • 接口隔离原则
      • 只在输入类和电梯类中实现了Runnable接口
      • 为了进一步提高可扩展性并降低电梯类的复杂度,可将getInOff()等从电梯类中分离使之成为接口,电梯实现该接口
      • 进一步思考,随着电梯调度任务的复杂,也可以将调度器中的分配任务抽离出来成为接口,由此可实现不同类型的调度器
    • 依赖倒置原则
      • 为了线程的安全性和简易性,只用了一个调度器接收两方面任务,出现了调度器类和电梯类相互依赖的关系

二. bug分析

相较于上单元的作业,本单元作业的评测全部使用本地实现的评测机,完全随机生成数据,可以在提交前对自己的程序进行多次测试;同时也可以很快发现别人的bug,极大地解放了生产力,提高了效率。
第二单元可能出现的bug如下

  • 死锁
    未在自己的程序和别人的程序中发现死锁。
  • CTLE
    未在自己的程序和别人的程序中发现CTLE。
  • Wrong Answer
    在第三次作业提交前发现过这种bug,原因是电梯线程在完成自己的任务后就结束了,未能考虑到之后会有换乘任务,导致请求没有到达目的地。
    在第三次作业的互测阶段也找到了一位同学的代码出现了死循环,有几个请求一直在频繁地上下电梯。
  • RTLE
    这是笔者最经常遇到的bug也是最令人头疼的bug,主要原因是没有考虑细致输入结束和线程结束之间的关系。
    在第一次作业的强测中最后一个点RTLE,互测也被hack到;第二次作业没有发现bug;第三次作业强测全部通过,互测因RTLE被hack中一次。

三. 心得体会

  • 线程安全
    线程安全最重要的是保护好共享对象,在本单元三次作业中,一直都是只实现一个调度器,就是为了减少共享对象的出现(只有两个函数用到了synchronized关键字),可基本避免死锁问题。
    三次作业中最令人头疼的问题就是电梯线程如何在合适时机自动结束,尤其是第三次作业,需要考虑换乘的请求,所以结束的条件相较于前两次有了很大的变化,需要在调度器中监测每个电梯的状态最后一起退出(推荐一种不同于发送特殊请求的结束方式System.exit(0)方便快捷省事,最重要的是它安全啊)。
  • 调度策略
    安全第一!安全第一!
    以后坐电梯再也不会吐槽电梯调度算法了
    由于请求的随机性,不见得有哪一种算法具有绝对的优势,在本单元的三次作业中共使用过两种算法LOOK算法和“找最近”算法(有点贪心的味道),总体上效果都还不错,在保证功能正确的前提下性能上还有很大的提上空间。
  • 架构优化
    不同于第一单元的作业,此次的三次作业没有显式的继承关系(不太需要),所以架构总体上来说比较简单。
    但从第三次的可扩展性分析来看,可以使用接口来降低复杂度提高可扩展性,实现架构的进一步优化。
posted @ 2020-04-15 17:32  骑着蜗牛追捣蛋  阅读(221)  评论(0编辑  收藏  举报