BUAA_OO 第二单元电梯调度作业总结

BUAA_OO 第二单元电梯调度作业总结

一、作业总结

这一次作业开始引入了线程的概念,并以电梯的形式进行考察。三次作业一层层递进的关系展开,与第一单元每次作业都进行重构相比,这一单元的电梯作业代码能够很好的得以复用,整体写下来很好地体会到了面向对象编程时有一个良好的建构设计的优势所在。

关于多线程设计模式

这三次的作业是一个很典型的生产者消费者模型,输入部分作为生产者,电梯根据需求进行调度属于消费者的部分,另外需要一个共享队列来保证生产者和消费者之间的通信。但由于本单元是我第一次进行多线程的编写,所以在刚开始我虽然不知道如何下手,但至少我知道了首先要构造生产者、托盘、调度器以及消费者,这相当于有了一个基本的大方向。另外,作为资源共享区的tray一开始十分让我头疼,因为我知道要保证我们的tray是线程安全的,因为我们可能会同时要往里放东西,又要从里面取东西;另外,如果共享区没有生产者生产的需求,那么消费者需要等待生产者生产,这就涉及到了wait()notifyall()的使用,下面我将具体分析。

SOLID分析

SOLID设计原则中:

SRP原则在elevator类中体现较差,本来内部调度单独采用了一个Dispatch类与elevator类进行交互,两者实际上完全耦合在一起,没有很好的做到功能上的区分,最后将Dispatch类的方法集成在elevator类中了。这两个类中方法规模及其庞大,在Metrics分析中疯狂飘红。

OCP原则在三次作业中有一定的体现,后面的两次作业都是多电梯,加入Controller类进行调度分配任务后实际上都是第一次作业的单电梯在执行任务,第一次作业基本没有改变,实现了第一次的程序的向外拓展,开闭原则得到了体现。

由于本单元未采用父类继承和接口实现所以没有LSP、ISP和DIP原则的体现。

第一次作业

第一次作业是三次作业中上手最为困难的一次,正如上文所述,由于对多线程设计的不熟悉,对生产者消费者模式也处于一知半解的状态,我做这次作业花费了较多的时间。

实现方法

主要的组成部件有:

  • 主线程:main函数,用来开启输入控制线程和电梯线程

  • 输入控制线程:用于控制输入,向主等待队列中添加请求(生产者)

  • 电梯线程:根据dispatch调度器产生的命令模拟电梯的运行,用于运行每个电梯的等待队列中的请求(消费者)

  • 主等待队列WaitList:一个线程安全的共享对象,设置AddRequestgetEleByIndex方法来放入、拿走请求,并使用wait()notifyAll()来阻塞或唤醒访问该共享对象的线程

调度策略

  • 在单部电梯执行任务的策略上,我使用了类似LOOK方法(似乎我理解的Look算法和同学的理解有所出入),总的宗旨是电梯选定一个主任务运送乘客,在每一个楼层都进行判断,在任务队列中有与当前电梯运行方向相同时进行响应,最后抽象出了一个执行任务的循环过程。

  •     while (dispatch.findMain()) { //满足要求寻找主任务
           dispatch.pickMain(); //接主任务
           dispatch.runMain(); //执行主任务
      }
  • 具体的实现也比较容易,选定主任务时按照Look算法,查看当前电梯状态时应该选取的主任务:

            boolean findSameDirSameSide = false;//priority = 1 
           boolean findDiffWaySameSide = false;//priority = 2
           boolean findDiffWayDiffSide = false;//priority = 3
           boolean findSameDirDiffside = false;//priority = 4

    按照优先级去队列中尽量寻找与电梯的方向相同,并且相对位置最为合适的任务作为主任务。实际上这样的操作在效率上已经相当的不错,相对于随机寻找主任务大概有近20s的大幅提升,优化效果我已经非常满意。

  • pickMainrunMain函数实现更为容易,与共享队列进行交互,主要是模拟在执行任务的过程中在每一个楼层乘客的上下电梯的行为。上电梯的乘客在未下电梯之前处于subReqs队列中,每一层会对subReqs进行遍历查找需要下电梯的乘客。需要注意的一点时,主任务到达目的地后,如果subReqs中还存在任务,那么直接随机选取一个任务重新作为主任务即可。

  • 一个循环过后在去寻找新的主任务,整个过程看起来还是十分清晰的。

  • 程序结束的判定:输入线程终止且电梯运行完毕。这次多线程的主要难点之一就是如何在避免暴力轮询的前提下能够使得程序得以成功的退出。 具体的判断上,本次判断的共享变量done的产生位于RequestQueue也就是共享队列中的AddRequest方法:

        public synchronized void AddRequest(PersonRequest req) {
           if (req == null) {
               setDone(true);
          } else {
               requestList.add(req);
          }
           notifyAll();
      }

     

    • 信号量done置位使得input线程中是否结束的while循环判断条件为真,input线程成得以功退出。

    • 电梯进程的调度器dispathchfindMain()方法主要是用来向共享队列查询是不是还有主任务MianReq,具体来说是调用了共享队列的waitQueue()方法

          public synchronized boolean waitQueue() {
             if (requestList.isEmpty()) {
                 while (requestList.isEmpty() && !done) {
                     try {
                         wait();
                    } catch (InterruptedException e) {
                         e.printStackTrace();
                    }
                }
                 if (done && requestList.isEmpty()) {
                     return true;
                }
            }
             return false;
        }

      如果共享队列不为空直接返回false,证明还没有结束。如果队列为空并且done编程为false,说明输入进程可能还会有输入,此时进程交出锁,进入等待状态,如果有新的输入将被唤醒,如果是由于requestList.isEmpty()被唤醒,返回false,进程继续;如果满足了条件done && requestList.isEmpty(),队列为空且已经收到结束信号,返回true,得到true后的dispatch.findMain()会使得电梯线程的大循环退出,线程正常结束。

    • 这样的话通过waitQueue()方法的返回值,整个程序既不会出现暴力轮询长时间使用CPU的情况,又能够在结束时正常的退出。 但是,从这个较为复杂的过程上来看,线程退出可能在下述情况下出现错误的判定

    • 最后一个任务从主等待队列中取出,但还没有交给电梯的时候,事实上这也是我第一次作业互测被查出的bug。

UML类图

BUAA_OO 第二单元电梯调度作业总结_第1张图片

Metrics

Method ev(G) iv(G) v(G)
Dispatch.Dispatch(Elevator,RequestQueue) 1 1 1
Dispatch.PrintInAndOut(ArrayList,ArrayList) 1 5 5
Dispatch.arrived(PersonRequest) 1 1 1
Dispatch.checkInAndOut(int) 1 1 1
Dispatch.findMain() 2 18 19
Dispatch.getDistanceFromNowFloor(PersonRequest) 1 1 1
Dispatch.hasMain() 1 1 1
Dispatch.isDone() 1 1 1
Dispatch.moveOnce() 1 1 1
Dispatch.needGetIn(int) 1 7 7
Dispatch.needGetOut() 1 5 5
Dispatch.pickByGetIn() 1 6 6
Dispatch.pickByGetOut() 1 4 4
Dispatch.pickMain() 4 4 5
Dispatch.runMain() 1 4 4
Dispatch.towards(PersonRequest) 2 1 2
Dispatch.upOrNot(int,int) 3 1 3
Elevator.Elevator(RequestQueue) 1 1 1
Elevator.arrive() 1 1 1
Elevator.close() 1 2 2
Elevator.getFloor() 1 1 1
Elevator.getStatus() 1 1 1
Elevator.in(PersonRequest) 1 1 1
Elevator.move() 1 2 3
Elevator.open() 1 2 2
Elevator.out(PersonRequest) 1 1 1
Elevator.run() 1 2 2
Elevator.setStatus(int) 1 1 1
Input.Input(RequestQueue) 1 1 1
Input.run() 1 2 2
MainClass.main(String[]) 1 1 1
RequestQueue.AddRequest(PersonRequest) 1 2 2
RequestQueue.RequestQueue() 1 1 1
RequestQueue.deleteEle(PersonRequest) 1 1 1
RequestQueue.getEleByIndex(int) 1 1 1
RequestQueue.getRequestList() 1 1 1
RequestQueue.getSize() 1 1 1
RequestQueue.isDone() 1 1 1
RequestQueue.realEmpty() 1 1 1
RequestQueue.setDone(boolean) 1 1 1
RequestQueue.waitQueue() 3 6 7

第二次作业

有了第一次作业的经验,第二次作业感觉轻松了不少。一个主要的坑点是本次作业出现了输入流安全方面的大问题,讨论区中提到不能使用两个不同的输入流,如果两个输入流同时存在并且其中一个没有关闭的话就会出现安全问题,解决方法可以考虑关闭输入流或者将一个输入流作为参数传入等等。

新加入或修改的组成部件有:

  • Controller:用来管理请求,向三个Elevator线程分配任务,里面加入一个总的队列,负责存放input进程输出的需求,行为逻辑与Elevator中的共享队列类似,我选择直接使用了同样的tray共享队列,减少了无用代码的书写。

  • Dispatch:加入了对电梯进入人数的限制。packMain()阶段限制最大人数为电梯负载的最大人数 - 1(避免接不到mainReq的情况存在),runMain阶段不超过最大负载即可.

  • Controller使用了被我称为大平均的分配方法,在进行动态调度较为困难的情况下,一个平均算法使得每个电梯运送的人数固定在10-25人左右,在第一次作业的算法还算不错的前提下,面对随机性很高的数据时这种分配方法还是收到了奇效,最后的结果令人满意。这也启示我在没有很好策略的情况下,将需要平均分配也不失为一种不错的选择,当然如果有能力找到更好的选择时,尽量要多多思考能够找到更优解的方法,毕竟大平均永远成为不了最好的策略。

  • 程序结束的判定:在Controller的队列发出可以结束的信号之后,退出while后,向每一个电梯线程发出结束的命令,即AddRequest(null).

    for (int i = 0; i < elevatorNum; i++) {
               subList.get(i).AddRequest(null);
    }

UML

 BUAA_OO 第二单元电梯调度作业总结_第2张图片

Metrics

Method ev(G) iv(G) v(G)
Controller.Controller(int,RequestQueue) 1 2 2
Controller.controlWhenMore() 5 8 9
Controller.controlWhenOne() 1 3 3
Controller.judgeToDispatch(int,int,PersonRequest) 5 3 5
Controller.run() 1 6 6
Controller.towards(PersonRequest) 2 1 2
Dispatch.Dispatch(Elevator,RequestQueue) 1 1 1
Dispatch.PrintInAndOut(ArrayList,ArrayList) 1 5 5
Dispatch.arrived(PersonRequest) 1 1 1
Dispatch.checkInAndOut(int) 1 1 1
Dispatch.findMain() 2 18 19
Dispatch.getDistanceFromNowFloor(PersonRequest) 1 1 1
Dispatch.getReqNum() 1 1 1
Dispatch.hasMain() 1 1 1
Dispatch.isDone() 1 1 1
Dispatch.moveOnce() 1 1 1
Dispatch.needGetIn(int) 3 7 8
Dispatch.needGetOut() 1 5 5
Dispatch.pickByGetIn() 3 6 7
Dispatch.pickByGetOut() 1 4 4
Dispatch.pickMain() 4 4 5
Dispatch.runMain() 1 4 4
Dispatch.setMainReq(PersonRequest) 1 1 1
Dispatch.towards(PersonRequest) 2 1 2
Dispatch.upOrNot(int,int) 3 1 3
Elevator.Elevator(SubRequestQueue,char) 1 1 1
Elevator.arrive() 1 1 1
Elevator.close() 1 2 2
Elevator.getDir() 1 1 1
Elevator.getFloor() 1 1 1
Elevator.getPeopleNum() 1 1 1
Elevator.getTotalNum() 1 1 1
Elevator.in(PersonRequest) 1 1 1
Elevator.inPeople(int) 1 1 1
Elevator.isDispatchable() 1 1 1
Elevator.isRunning() 1 1 1
Elevator.move() 1 2 5
Elevator.open() 1 2 2
Elevator.out(PersonRequest) 1 1 1
Elevator.outPeople(int) 1 1 1
Elevator.run() 1 2 2
Elevator.setDir(int) 1 1 1
Elevator.setDispatchable(boolean) 1 1 1
Elevator.setMainReq(PersonRequest) 1 1 1
Input.Input(RequestQueue,ElevatorInput) 1 1 1
Input.run() 1 2 2
MainClass.main(String[]) 1 1 1
RequestQueue.AddRequest(PersonRequest) 1 2 2
RequestQueue.RequestQueue() 1 1 1
RequestQueue.deleteEle(PersonRequest) 1 1 1
RequestQueue.getEleByIndex(int) 1 1 1
RequestQueue.getRequestList() 1 1 1
RequestQueue.getSize() 1 1 1
RequestQueue.isDone() 1 1 1
RequestQueue.realEmpty() 1 1 1
RequestQueue.setDone(boolean) 1 1 1
RequestQueue.waitQueue() 3 6 7

第三次作业

第三次作业加入了换乘,以前作业中我直接使用课程组提供的PersonRequest类不能满足我对于换乘设计的要求,因此重新封装了一个Person类,这个类可以根据自己的起点和终点自动计算出自己的换乘路线,并可以返回现在是处于换乘前的状态还是换乘后的状态以及他当时需要分配到的电梯。具体流程是,输入时产生Person类进行解析,得到是否需要换乘,以及换乘的路线。在到达指定的楼层时,判断这个乘客还需要换乘与否,需要换乘的话也要讲需要换乘的标志位置为false,重新写回到Controller的队列当中,重新按照电梯类型进行分配。

  • 策略:将目的地是-3至-1层和目的地是16-20层的任务划分给A,其余任务由BC承担。B运送偶数层,C运送奇数层,采用类似第二次作业的办法,保证需求的平均性以期待更好的性能。

  • 中转层:1层、9层与15层。9层对于A电梯进行一定的特判,与9有关的需求禁止进入A电梯。

  • 这些操作没有引入到前两次作业实现的代码中,基本上都是在Person类中实现,修改的部分也仅仅是队列中存放对象的类型。

  • 程序结束的判定:程序结束的判定存在一个矛盾:按照之前的设计,elevator进程的停止在Controller调度结束之后,但是Controller的停止条件又依赖elevator进行判断,因为电梯里面只要还有乘客那么久有换乘之后再度写入Controller大队列的可能。 因此我在Controller大队列中建立了一个计数器Counter,当电梯内部队列新增一个请求时,counter自增1,当电梯内部队列中一个需要换乘的乘客下电梯时,counter自减1。Controller大队列被所有的电梯共享。当满足如下条件时

    public synchronized boolean waitBigQueue() {
           if (requestList.isEmpty()) {
               while (requestList.isEmpty() && (!done || (counter != 0))) {
                   try {
                       wait();
                  } catch (InterruptedException e) {
                       e.printStackTrace();
                  }
              }
               if (done && requestList.isEmpty() && (counter == 0)) {//结束条件在这里
                   return true;
              }
          }
           return false;
      }

    退出Controller.

UML

 BUAA_OO 第二单元电梯调度作业总结_第3张图片

Metrics

Method ev(G) iv(G) v(G)
Controller.Controller(RequestQueue) 1 2 2
Controller.addElevator(String,String) 1 1 1
Controller.allElevatorDone() 4 3 4
Controller.judgeToDispatch(int,int,PersonRequest) 5 3 5
Controller.run() 6 7 10
Controller.towards(PersonRequest) 2 1 2
Dispatch.Dispatch(Elevator,RequestQueue,RequestQueue) 1 2 3
Dispatch.PrintInAndOut(ArrayList,ArrayList) 1 6 6
Dispatch.arrived(PeopleWithIq) 1 1 1
Dispatch.checkInAndOut(int) 1 1 1
Dispatch.findMain() 2 18 19
Dispatch.getDistanceFromNowFloor(PeopleWithIq) 1 1 1
Dispatch.hasMain() 1 1 1
Dispatch.isDone() 1 1 1
Dispatch.moveOnce() 1 1 1
Dispatch.needGetIn(int) 3 7 8
Dispatch.needGetOut() 1 5 5
Dispatch.pickByGetIn() 3 6 7
Dispatch.pickByGetOut() 1 4 4
Dispatch.pickMain() 4 4 5
Dispatch.runMain() 1 4 4
Dispatch.setMainReq(PeopleWithIq) 1 1 1
Dispatch.towards(PeopleWithIq) 2 1 2
Dispatch.upOrNot(int,int) 3 1 3
Elevator.AddRequest(PeopleWithIq) 1 1 1
Elevator.Elevator() 1 1 1
Elevator.Elevator(SubRequestQueue,String,String,RequestQueue) 1 2 3
Elevator.arrive() 1 1 1
Elevator.close() 1 2 2
Elevator.getDir() 1 1 1
Elevator.getFloor() 1 1 1
Elevator.getPeopleNum() 1 1 1
Elevator.getSel() 1 1 1
Elevator.in(PeopleWithIq) 1 1 1
Elevator.inPeople(int) 1 1 1
Elevator.isRunning() 1 1 1
Elevator.move() 1 2 5
Elevator.open() 1 2 2
Elevator.out(PeopleWithIq) 1 1 1
Elevator.outPeople(int) 1 1 1
Elevator.run() 1 2 2
Elevator.setDir(int) 1 1 1
ElevatorQueue.AddEle(Elevator) 1 1 1
ElevatorQueue.ElevatorQueue() 1 1 1
ElevatorQueue.getEleByIndex(int) 1 1 1
ElevatorQueue.getSize() 1 1 1
Input.Input(RequestQueue,ElevatorInput) 1 1 1
Input.run() 1 4 4
MainClass.main(String[]) 1 1 1
Output.println(String) 1 1 1
PeopleWithIq.PeopleWithIq(PersonRequest) 1 1 1
PeopleWithIq.atTransferFloor(int) 1 1 3
PeopleWithIq.elevatorAfloor(int) 1 1 2
PeopleWithIq.elevatorBfloor(int) 1 2 2
PeopleWithIq.elevatorCfloor(int) 1 2 2
PeopleWithIq.getFirstFromFloor() 1 1 1
PeopleWithIq.getFirstToFloor() 1 1 1
PeopleWithIq.getFromFloor() 2 2 2
PeopleWithIq.getLastFromFloor() 1 1 1
PeopleWithIq.getLastToFloor() 1 1 1
PeopleWithIq.getPersonId() 1 1 1
PeopleWithIq.getToFloor() 2 2 2
PeopleWithIq.getTransferFloor(int,int) 6 5 15
PeopleWithIq.isNeedTransfer() 1 1 1
PeopleWithIq.needGetIn(int) 2 2 2
PeopleWithIq.needGetOut(int) 2 2 2
PeopleWithIq.needTranster(PersonRequest) 1 5 5
PeopleWithIq.setNeedTransfer(boolean) 1 1 1
PeopleWithIq.toWhichElevator() 3 2 3
PeopleWithIq.transferFloorToType(int) 3 2 3
RequestQueue.AddRequest(PeopleWithIq) 1 2 2
RequestQueue.RequestQueue() 1 1 1
RequestQueue.addCounter() 1 1 1
RequestQueue.deleteEle(PeopleWithIq) 1 1 1
RequestQueue.getEleByIndex(int) 1 1 1
RequestQueue.getRequestList() 1 1 1
RequestQueue.getSize() 1 1 1
RequestQueue.isDone() 1 1 1
RequestQueue.realEmpty() 1 1 1
RequestQueue.setAlldone(boolean) 1 1 1
RequestQueue.setDone(boolean) 1 1 1
RequestQueue.subCounter() 1 1 1
RequestQueue.waitBigQueue() 3 7 9
RequestQueue.waitQueue() 3 6 7

二、bug分析

公测

本次公测我没有出现bug

互测

第一次作业:被找到了4个同质的bug,表现都是最后一批人根本没有进行相应,仿佛他们不存在一样,在本地我进行测试也没有复现成功。但是在同学的提示下,我对我的线程结束条件又进行了分析,发现了自己的问题,原因是我的最开始的waitQueue()方法存在问题,具体如下:

 public synchronized boolean waitQueue() {
       if (requestList.isEmpty()) {
           while (requestList.isEmpty() && !done) {
               try {
                   wait();
              } catch (InterruptedException e) {
                   e.printStackTrace();
              }
          }
           if (done) { //这个判断条件实际上是不严谨的
               return true;
          }
      }
       return false;
  }

与修改后的代码进行对比,主要是进程被唤醒后退出while循环后面的判断条件进行了修改。那么我第一次的这种写法问题出在哪里了呢?实际上,我当初考虑的是只要退出了这个循环,就是由于done已经被设置为true但是实际上,最后的输入可能也没有被Elevator线程读走,具体就表现为最后一条输入与结束的null相隔极为接近,那么这时,虽然队列不为空,但是由于done的缘故,电梯线程还是会退出,那么最后的这么一条命令就不会得到响应,结果自然而然是错误的。出现这样的问题是我的惯性思维导致的,当时我的思维停留在单线程阶段,认为前面既然有了一条判断if (requestList.isEmpty()),那么退出循环后如果donetrue的话,原因只可能是程序需要结束了,但实际上,该部分被notifyall()唤醒后是用while循环内部继续执行。根本就没有我所认为的if判断的先决条件,实际上waitwhile连用也是为了保证线程被唤醒后的执行位置。

第二三次作业:什么也没有发生。

关于测试

本次电梯作业要求在相应的时间准确进行输入,于是我使用了同学用python写的定时输入的程序进行本地的测试,它可以向打包好的jar输入完整格式的请求。在进行debug时,我主要还是使用println对程序进行跟踪,配合定时输入程序,我也能较为容易的找到自己程序所在的位置。

在第三次作业中,找到了学长的一个正确性检测程序,配合定时输入程序,对输入输出进行重定向后可以判断程序的正确性,完整的项目在https://github.com/noobyxy/elevator_tester.git

测试策略

利用该程序我成功在第三次的互测中hack到了小组中三个成员的bug,多为到达了不能到达的楼层,程序不能正常停下等等bug,利用随机的数据处理进行大量测试就能收到很好的效果。

另外,由于第二三次的作业有携带人数的要求,我们可以构造大量同时让同种电梯携带的数据进行hack,对于我这种在接主任务中同样携带的程序来说,一样可以构造对应的数据进行hack,我第二次作业的bug没有被触发出来也是非常的幸运。

三、心得体会

线程安全

线程安全在多线程设计中属于非常重要的一个环节,出现线程安全问题直接宣告多线程设计的失败。 多线程很可能会有多个线程对共享资源的访问时互斥性以及线程间的死锁问题,这些问题实际上都有很好的解决方案。

  • 在设计时考虑到线程之间的同步互斥关系

  • 考虑不同线程和共享对象之间的关系

  • 合理上锁,在帮同学debug时,发现他对于锁的理解可能有些问题,在不同的类里面频繁上锁。我的理解是 任何需要在线程之间交互的变量都应该集成内聚为共享变量类:就本次作业而言,共享变量就是几个共享的调度队列,在调度队列中实现能够判断线程安全结束的方法。这些都被线程共享使用,生产者消费者调用自己的方法时,最后的操作必然会归结到对于共享队列的操作,最后都会调用共享队列中的方法,因此上锁的对象仅仅是共享队列已经足够了,如果使用了多余的锁,可能是考虑的还不够充分,在我看来本次的作业都可以通过调用共享队列中带锁的方法保证线程安全

  • 考虑所有情况,避免不安全的情况出现。这单元的第一的bug是一个很好的教训,也感谢那位帮我找到问题的同学让我及时止损,在第二次第三次作业没有出现问题。

代码复用

这次多线程作业让我感到非常满意的一点是我的代码得到了充分的复用,实际上,在面临新的任务要求时,要对于之前架构的方方面面进行新的考量,避免与新的要求发生冲突 。能够很好的复用到以前的代码,避免重复造轮子也是一种我们需要掌握的能力。 同时也让我认识到一个好的架构对于工程实现是多么的重要,对这个架构进行功能上的增添即可实现新需求的满足。

一些妥协

在帮助同学debug以及公测的过程中,我发现有一部分同学为了避免轮询的发生,手动让在共享队列中进行查询的函数sleep(sometime),这种方法确实可以满足这次作业的需要,不会出现cputle的情况,但是实际上,我认为这种实现方式是一种很大的妥协,它不是我们课上讲到的、以及课程组推荐使用的wait()\notifyall()函数的正确使用方式 ,这种形式只是一种不会cputle的轮询,我认为这种做法不是十分合理。

在本单元的学习过程中,我认为自己的收获是相当大的,了解了多线程的设计模式,了解了一些设计上的原则,祝自己的OO之旅更为顺利吧

 

 

你可能感兴趣的:(BUAA_OO 第二单元电梯调度作业总结)