Object Oriented Homework Summary(2)

OO第二单元总结(电梯问题)

    论滑铁卢的诞生



 

一、程序结构分析

 

第一次作业(单电梯无捎带)


  本次作业需要完成对单个电梯从1-15层的调度。无性能分。

(1)UML:

 

一目了然 不言而喻


(2)功能概述

  这次作业用得上多线程?

  这次作业设置了五个类:Main,TaskHandler,Elevator,GetTask以及SendTask.其中Main是主线程,TaskHandler阻塞式读取输入的数据,把输入的PersonRequest类转化成GetTask对象和SendTask对象并传入到Elevator对象的任务队列中。而Elevator对象则暴力轮询任务队列TaskBuffer来判断是否能够搭载相应任务的乘客。

  我如此设计的主要目的在于为hitchhiking功能的实现提供基础。毕竟如果电梯要想搭载能够hitchhike的乘客肯定需要对当前全部的任务有访问的能力。尽管从后面的作业(多电梯)来看,将全部任务放入一个电梯是糟糕的设计,但这个设计思路仍然能够一定程度保证效率,并且在线程安全角度也是较优的选择(分开阻塞两个Buffer的读写线程)。

  主要的缺点在于暴力轮询。尽管我周围的有些人甚至将暴力轮询带到了第三次作业中(而且还pass了所有强测点ORZ),但暴力轮询从性能和cpu资源利用的角度分析并不是好的选择。更合理的架构是wait和notify建立的锁机制,然而如果锁设计的不佳那么也可能会出现死锁或忙等现象(比如我的第三次作业)。

 

第二次作业


  

  本次作业需要实现从-3到15层的单电梯调度。以运行结束时间作为性能分衡量标准。

(1)UML

 

(2)功能概述

  我真的没有把上次的UML搬过来。

  这次作业直接照搬了上次作业,原因很明显:我在上次作业中已经实现了捎带功能,因此直接复用再做微调是比较好的选择。

  这次作业依旧是五个类,依旧是一头进一头出的TaskHandler。总之基本一样,唯一不太一样的是对于负数层的处理。我处理的方法是将负数层加3,正数层加2,如此一来全部层数便可以被映射到从零开始的数组当中,并且也不存在从-1层到+1层的跳跃了。

 

三次作业


  这次作业要求三部可停靠楼层不同运行速度不同的电梯完成-3到20层的调度任务。依旧是以最终完成时间作为性能指标。

(1)UML

  

  这类图还不如不放呢:)

(2)功能概述

  这次作业真的需要用到多线程了。

  由于和前几次功能所需的架构截然不同,我基本重构了之前的设计,但是许多细节比如任务类以及楼层映射的设计依旧保留下来了。这次作业一共设置了8个类。

  线程类:

  Main:类如其名。

  InputBuffer:输入模块。获取输入,处理成Task类并传递到Distributor中(名字起的不好,我知道)

  Distributor:调度器。将输入的任务进行分发调度。如果任务能够被当前某电梯捎带那么就直接发派,否则加入到暂存队列中;暂存队列会在每次电梯状态改变时被遍历进行重新分配。

  Elevator:电梯本梯。拥有一个Hitchhike队列,仅仅保存可以被捎带的任务。在每次到达新楼层时时刷新状态并通知Distributor。

  Toss:管道。由于线程既要保证安全又要追求效率,于是为了不让线程间传递消息被阻隔,于是用Toss类来作为等待锁的中转,原本线程可以继续运行(And thus,the tragedy begin)

  数据类:

  Task:类如其名。保存了一个请求的完整信息(就是重写了一遍),加入了任务方向等信息。

  GetTask:类如其名。保存了一个上电梯请求的信息(id-floor)。除此之外,其中包含一个SendTask的引用作为其中的一部分,当完成了GetTask时电梯会获取这个对应的SendTask并加入到队列中。

  SendTask:类如其名。保存了一个下电梯请求的信息(id-floor)。除此之外,还包含了一个Task类的引用。如果某任务无法凭单个电梯完成或是从效率角度不优,那么一个电梯需要将一个任务拆分成两个。这个Task类的引用就是保存了任务剩余部分的信息。

  处理流程图如下:

    细节说明:

    1、Can be distribute?

      在这部分我采用了一个时间指数的设计。每个电梯根据当前任务会有两个时间性能得分

  public int getRemainTime() {
        int remainTime = 0;
        //System.out.println(this.name + " can reach? " + availList[t.getTo()]);
        remainTime += (des - floor) * movStatus * speed;
        remainTime += taskCount * doorDelay;
        //System.out.println(name + " remain:" + remainTime);
        return remainTime;
    }

 

 

 

 

    public int getNewTime(Task t) {
        int newTime = getRemainTime();
        int from = t.getFrom();
        int to = t.getTo();
        if (!isGetable(t)) {
            return 999999999;
        }
        if (!availList[t.getTo()]) {
            newTime += 9999999;
        }
        if (!isHitchable(t) || movStatus == 0) {
            newTime += 2 * doorDelay;
            newTime += Math.abs(floor - from) * speed;
            newTime += (to - from) * t.getDir() * speed;
        } else {
            if (!taskFloor[from]) {
                newTime += doorDelay;
            }
            if (!taskFloor[to]) {
                newTime += doorDelay;
            }
            if (movStatus == 1 && to > des) {
                newTime += (to - des) * speed;
            } else if (movStatus == -1 && to < des) {
                newTime += (des - to) * speed;
            }
        }
  //System.out.println(name + " new:" + newTime);
   return newTime;
  }
 

    这两个方法作用如下:

    1、getRemainTime():获取当前hitchhike队列中任务执行完所需时间

    2、getNewTime(Task task):获取当当前队列加入任务task后完成所需时间

    涉及到任务分配时,我的原则是保证全部电梯完成所有任务最长时间最短为优先,也就是贪心算法

    图示如下:

    可以看到:尽管C电梯完成当前队列最快,但是加入新任务后反而会让总时间变长;B电梯任务最多,但是加入新任务后 总完成时间是把任务分给其他电梯所耗时间最短的。因此采用这样的设计可以在贪心的基础上最大限度地减少总运行时间(不过实际效果并不咋地,所以我在扯些什么)

  2、转发机制

    之前提到了,如果一个电梯不能或不应独自完成一个任务时应当把任务拆解成两部分交给别的电梯完成。这需要一个线程安全的任务转发机制。这是目前的共享数据域,对应的访问方法以及可以调用方法的类:

    

    我早晚有一天要栽在起名上

    如图所示,一共有三个共享数据域,每个访问区配置了两个访问方法。尽管这个图看上去一目了然,但在读写锁的设计上我仍然写成了一团乱麻依旧出现了不少问题。其中最主要的就是锁的范围。出于方便考虑,我的每一个任务队列都是由ArrayList类构造,由于ArrayList类不支持在遍历时修改,因此我采用先复制再遍历的方法,即将当前队列复制到一个临时队列,然后遍历临时队列,需要删除时把原队列对应元素删除即可。这样设计的问题在于克隆队列应当与遍历队列处于同一个锁之中,而我为了效率将其拆分成两个锁,这就导致在遍历数组时发生插入新任务事件无法被阻塞。

 

二、线程安全与效能

  线程安全与效率?


  目前在多线程方面的实践给了我一个印象:线程的安全性与效率性是矛盾的。比如java比较常用的锁syntronized,在具体线程实现的时候如果把整个对象锁住无疑可以保证其他所有互斥线程的执行,但也会导致本来可以共享访问的线程被无端阻塞(就像被所在银行外面的客户)。然而如果缩小锁的范围,保证只在关键数据的访问互斥,并且对于不同数据采用不同的锁,如此从效率上的确做到了最大化的并行,然而却会导致死锁或者忙等的可能性。(鸵鸟大法好)

  因此关键问题在于细节上的设计。避免死锁只需要排除死锁的必要条件。尽管在整个程序范围内做到从根本上避免死锁难度较大,但是可以通过trade off来达到一个较优的状态。

posted @ 2019-04-22 09:50  Socrates1232  阅读(139)  评论(0编辑  收藏  举报