BUAA OO 第二单元总结——多线程电梯

BUAA OO 第二单元总结——多线程电梯


一、同步块的设置和锁的选择

本单元第一次作业

本次作业为单部电梯,只有两个线程,一个是输入线程,一个是一部电梯。本次作业采用了简单的生产者——消费者模式,输入线程InputThread充当Producer,电梯类Elevator充当Consumer。因此二者的共享对象需要采用加锁即sychronized,共享对象为一个等待队列类RequestQueue,里面队列容器为ArrayList容器,为线程不安全的容器,因此对该容器的增加、删除操作均需要使用sychronized修饰语进行同步保护,形如:

private final ArrayList<PersonRequest> requests = new ArrayList<>();

public void addRequest(PersonRequest request) { // Producer do
       synchronized (requests) { // lock
           requests.add(request);
           requests.notifyAll(); // awake and unlock
      }
       // do something
  }

public PersonRequest getRequest() { // Consumer do
       if (requests.isEmpty()) {
           synchronized (requests) { // lock
               try {
                   requests.wait(); // wait and unlock
              } catch (InterruptedException e) {
                   e.printStackTrace();
              }
          }
      }
       return requests.get(0);
  }

当容器为空时,消费者调用getRequest方法,进入同步块中释放锁,并等待,等待生产者进入该同步块添加元素并唤醒消费者。所有相关操作将monitor设置为requests,保证了线程安全。

本单元第二次作业

由于本次作业为多电梯同时运行,共享对象的访问更加频繁,为了安全起见,将同步块的monitor从所有属性改为该对象this,即对非静态方法进行加锁sychronized。如上改为:

public synchronized void addRequest(PersonRequest request) {
   requests.add(request);
   notifyAll();
}

public synchronized PersonRequest getRequest() {
   if (requests.isEmpty()) {
       try {
           wait();
      } catch (InterruptedException e) {
           e.printStackTrace();
      }
  }
   return requests.get(0);
}

同时笔者增加了分派器Dispatcher作为输入队列的消费者,每个电梯的等待队列的生产者,总体的锁与第一次作业相差无几,只是分派器既充当消费者也充当生产者。

本单元第三次作业

本次作业新增了不同种电梯,为此增加了换乘的功能。第二次中所有的队列增加和减少都成单向的,即

  • 输入线程只考虑总队列的输入

  • 分派器只管总队列的输出和电梯队列的输入

  • 电梯只管电梯队列的输出。

而增加了换乘之后,分派器又需要考虑来自电梯的输入,电梯也要考虑向总队列的输入。因此,共享对象的交互变得非常频繁,因此在原有的架构上,没有对RequestQueue类增加更多的加锁操作。而是在线程类对总队列,电梯等待队列的对象进行加锁。例如开关门操作:

public void openInOutClose(int recentFloor) {
  synchronized (mainQueue) { // 需要换乘到总队列
           inMyPerson(request, recentFloor);
      }
       synchronized (waitQueue) { // 等待队列
           synchronized (localQueue) { // 已经进入电梯里的队列
               inPerson(elevator.getMaxNum(), recentFloor);
          }
      }
  }

同时对每处加锁的顺序均为mainQueue->waitQueue->localQueue,保证了不出现死锁的情况。


二、调度器设计

第一次作业由于只有单部电梯,增加调度器意义不大,因此未设计调度器。

第二次作业和第三次作业的调度器设计相差不大。其功能和运行逻辑主要为:

  • 判断总队列是否不会再有新的请求,若不会,则通知每部电梯结束,并结束调度器线程;若会,则继续下一步

  • 从总队列中获取新的请求

  • 根据电梯的到达模式、电梯的型号进行分派请求,即将请求从总队列中移除,并加入到对应的电梯等待队列中(完成唤醒电梯)

第二次作业,由于电梯种类相同,所以相比于自由竞争,利用分派器去模拟每部电梯的运行时间获得最短运行时间的电梯并将请求送进去,这样可以最大程度的优化性能。

但是,笔者个人认为,第三次作业相比于第二次作业的可拓展性方向主要是换乘机制,拥有调度器的架构比自由竞争的架构,在实现该功能的情况下很明显更符合面向对象的程序设计思路,但是通过强测后的结果来看,自由竞争的方法比使用调度器来是实现分派、换成的方法更加的性能高效,这显然不太符合增量式开发和面向对象思想的优势,但也有可能是笔者换乘策略的问题,这样就显得调度器是非常多余的

因此个人非常建议课程组对有无调度器设计这方面做出比较好的决策,即使性能优化方面确实很复杂,也可能存在过度优化的可能,但是总不能实现了调度器以及换乘的功能到最后性能分也不理想吧。


三、第三次作业架构设计和可扩展性

UML类图

|- MainClass:主类 
|- Dispatcher:调度器,继承自Thread类
|- InputThread:读入线程,继承自Thread类
|- Elevator:电梯接口
|- ElevatorA、B、C:电梯A类、B类、C类,继承Elevator接口和Thread类
|- ElevatorFactory:电梯工厂
|- ElevatorScheduler:抽象到达策略类
|- RandomScheduler、MoringScheduler、NightScheduler:三种到达策略类,继承自ElevatorScheduler
|- RequestQueue:队列
|- Floor:楼层

重要的类和功能:

ElevatorFactory电梯工厂

  • 根据读入线程的增加电梯指令以及初始化生产电梯,将一部新的电梯Elevator和一个新的到达策略类绑定,并开启该电梯的线程。

Dispatcher调度器

  • 管理每部电梯的策略类Schedulers

  • 管理总队列mainQueue(RequestQueue实例化对象)

InputThread读入线程:

  • 向电梯工厂发送到达模式请求

  • 向电梯工厂发送增加电梯请求

  • 向总队列mainQueue(RequestQueue实例化对象)发送增加乘客请求

ElevatorScheduler到达策略类:

  • 管理总队列mainQueue(RequestQueue实例化对象)

  • 管理本电梯的等待队列waitQueue(RequestQueue实例化对象)

  • 管理本电梯的乘客队列localQueue(ArrayList实例化对象)

Elevator电梯类:

  • 从策略类中获得下一请求,运行

UML协作图

复杂度

可以看到圈复杂度高的方法都是开关门以及请求的传递有关,在分派器中分派请求时用到了较多的对电梯的判断以求得最短时间的电梯,以及电梯中到达楼层时进出人员的判断。但总体来讲,相较于第一单元,架构的合理性可以使得复杂度降低。

本次架构设计从第一次到第三次,没有重构且均未有大量的改动,第二次主要改动为增加分派器,第三次主要是换乘功能的添加,相比于第一单元,面向对象的程序构造思想更加熟练

对于可扩展性笔者个人认为,本单元的架构能够很好的满足不同到达模式,不同电梯,不同楼层的需求,仅需要将需求送入ELevatorFactory电梯工厂,再更改电梯的参数,或者新增到达策略并继承ElevatorScheduler,重写部分调度方法即可。


Bug分析

第一次作业

由于为了将当前楼层的请求压入电梯,在等待队列类中都没有轮询,而在策略类里有一处while,导致CPU超时,非常可惜的bug。

第二次作业

由于第一次作业的电梯运行逻辑的问题,导致有一个点运行时间超过30秒,导致运行时间超时。另外还有一个点运行超时可能是发生死锁了,但由于本地复现不出来,所以在第三次作业中多加了很多锁来避免再次出现死锁。

第三次作业

有一个点因为换乘逻辑的问题导致运行楼层越来越高都怪换乘,最终没有正确结束电梯。

总体来说,三次作业的bug都可以避免,也是本单元的主要遗憾。


发现他人bug的方法

由于本单元多线程的特殊性,即使本地利用评测机产生随机数据,并且同时控制台多开的情况下找到了bug,交上去大多数也都未hack成功,三次作业只有第二次成功hack了同组的5个点。


心得与体会

笔者在第三单元之前未曾接触过多的多线程的程序设计,因此在第一次作业的过程中花了很多时间去学习多线程的交互和如何保证共享对象的线程安全。虽然第二次作业依旧发生了线程不安全的情况,但是给了我很多启发,如何放置同步块和锁,既能保证安全,又能保证性能,还能符合面向对象的设计,对于笔者来说是值得继续探讨的。

同时,笔者认为本单元还有很多对于课程有些可以继续改进的地方,比如在同一次程序中加入不同的到达模式,以及对调度器这个处于很尬尴的设计进行一些调整,比如设计合理的测试样例来让实现换乘的可以体现出优势。

值得高兴的是,笔者认为第二单元从架构和设计的历程来说要比第一单元简单、思路清晰,希望接下来两个单元再接再厉。

posted @ 2021-04-22 23:42  Fight扬尘  阅读(106)  评论(0)    收藏  举报