OO第二单元作业(多线程电梯)总结

一、作业简要说明


    在第二单元的OO课程中,我们开始学习并且逐渐掌握多线程的相关内容。在第二单元的三次作业中,难度是逐渐上升的,但是核心是不变的,如何进行高效的调度,避免在资源竞争中产生死锁。

    作业5:

  • 单部电梯的先来先服务算法
  • 如何区分对象,以及构建线程
  • 线程间如何通信

    作业6:

  • 单部电梯的可捎带服务算法
  • 确定电梯的方向
  • 线程间通信,需求队列的构造

    作业7:

  • 多部电梯的动态调度算法
  • 电梯停靠楼层不一致,速度不一致,容量不一致
  • 多部电梯存在资源的竞争,如何避免死锁
  • 需求中存在有转乘,如何高效调度

    三次作业,前两次会比较简单,只需要控制好线程间的通信,即可满足相应的要求。而第三次作业则是需要对多部电梯间生成动态调度的算法,使得均衡不同电梯间的负载,以及如何高效快速的控制电梯进行人员接送等问题,优化难度较大。

二:设计策略


  从多线程的协同和同步控制方面,分析和总结自己三次作业的设计策略

  在三次作业中,主要的策略即构建通信机制,使得不同线程间信息互联,从而能够针对性的做出反馈,尝试过轮询,wait/notify,synchronnized等方法。

2.1 作业5 单部电梯先来先服务算法

  在本次的作业中,一共涉及五个类:Main,GetInput,Elevator,Schedule,Requests,按照类的名字大概可以知道不同类的功能,分别为:主类,输入类,电梯类,调度类,请求队列类。

  在本次的作业中,由于仅需要实现先来先服务的算法,故仅需在获取得需求后,由输入类,将需求存入请求队列中,调度器实时检查请求队列的情况,调度类(Schedule)检查请求队列非空的情况下,并且获取电梯(Elevator)状态(isRunning),此时若电梯处于运行状态,则继续轮询,否则将该需求移出请求队列(Requests),并且驱动电梯进行相应的运动,而电梯每次运动的结束,都返回将自己的状态isRunning置为False。

  从上述的描述中可以看出,本次实验中采用的是轮询的方法,其中,Requests类被输入类和调度器所共享,故请求队列需要将共享的一些方法,使用synchronized进行原子化操作,使其线程安全。这里仍然需要设计几个状态参量,如调度类存在状态参量是否输入结束,电梯存在状态参量是否停止,调度器则需要读取其状态参量,如请求队列为空,输入停止,电梯停止,此时调度器便通知电梯线程退出,自己也便退出。这些状态参量此次实验中我并没有使用synchronized线程安全化,因为本次实验中采用的是轮询的方法,并且仅有一方可进行写操作,若判断不生效,则等待下一次轮询即可。

2.2 作业6 单部电梯可稍带服务算法

  在本次的作业中,依旧是涉及五个类:Main,GetInput,Elevator,Schedule,Requests,类的作用同上第5次作业。

  在本次作业中,需要实验的是可捎带的算法,可捎带的算法,便不能每次仅给电梯发送单一请求,需要一次性给电梯发送当前的所有请求,电梯自身则需要构建两个数组,该数组长度即为电梯长度,用于衡量目前哪一个楼层有人在等待,以及电梯内的人需要前往的楼层,电梯每次上下层楼的时候,需要检查同方向是否有人在等待,以及同方向,是否有电梯内的人需要出,如果是则继续该方向,否则判断相反的放下给是否有人要进入或出,即反向。

  本次实验中采用wait/notify的策略,线程控制上,输入类获取输入,将输入加入到请求队列类中,并且提醒(notify)调度器类,调度器类读取请求队列中的需求,将请求加给电梯类,并且提醒(notify)电梯类执行,而调度器读取请求队列为空时,则等待(wait),电梯在当前电梯内没有人,也无等待的人的时候进入等待状态(wait)。同时需要维护一个状态量,输入类中的输入结束参量,调度器发现调度器退出,并且请求队列为空后,即向电梯发送退出请求,调度器自身便退出,电梯在自身在执行的过程中,不会检查收到的退出量,在自身的所有的需求都服务完毕后,电梯会检查该exit量,若要退出,则电梯也安全退出。

  本次实验中采用了wait/notify的策略,由于仅仅涉及到两两线程之间的通信,仅需要保证读写的顺序,就不会产生线程安全等问题,在本次实验中仅仅需要对需求进行通信,提醒各个进程执行自己所需要完成的操作。

2.3 作业7 多部电梯动态调度算法

  在本次作业中,涉及六个类:Main,GetInput,Elevator,Schedule,Requests,MyPersonRequest,这里新增了一个类,叫做MyPersonRequest类,这个是对PersonRequest的再封装,内容为两个PersonRequest,作用为将需要转乘的类,进行拆分,拆分成俩个需求,按顺序执行。

  在本次作业中,需要实验的是多部电梯的动态调度算法,多部电梯间相互独立,每部电梯,仅需要按照自己的需求执行即可,运行速度,即在上下楼层过程中控制sleep的时间,停靠的楼层,在电梯的内部不需要考察,由调度器将需求分配给相应可以完成对应需求的电梯,所以电梯中执行的任务一定是自己可以完成的。而对于电梯内负载的限制,给予人数的上限即可。电梯执行的过程中,执行的任务是封装过的MyPersonRequest,所以在该人出电梯后,可以根据该类,进行判断是否需要转乘,需要转乘时,则将转乘的需求,再次输入给调度器类,让调度器再次进行调度。

  根据上述的算法的描述,多线程的协同控制可表述如下:输入类获得输入,加入到请求队列中,并且提醒(notify)调度器处理,调度器检查输入类、电梯类、请求队列,如果输入类退出,电梯类不运行,请求队列为空,则表述调度器将退出,否则读取请求队列的下一个请求,封装成MyPersonRequest,发送(notify)给对应的电梯,电梯进行进行相应的需求的执行,执行完一条需求后,将可转乘的需求发送给调度器,提醒(notify)调度器再进行分配,自己的所有的需求执行完毕时,即进行等待(wait)状态,等待唤醒。

三:度量分析


  在度量分析中,采用五个度量参数,具体含义表述如下:

(1)ev(G):基本复杂度,用来衡量程序非结构化程度的,范围在[1,v(G)]之间,值越大则程序的结构越“病态”。非结构成分降低了程序的质量,增加了代码的维护难度。

(2)Iv(G):模块设计复杂度,用来衡量模块判定结构,即模块和其他模块的调用关系。软件模块设计复杂度高意味模块耦合度高,这将导致模块难于隔离、维护和复用。

(3)v(G): 循环复杂度,用来衡量一个模块判定结构的复杂程度,数量上表现为独立路径的条数,即合理的预防错误所需测试的最少路径条数。

(4)OCavg:类方法的平均循环复杂度。

(5)WMC:类方法的总循环复杂度。

3.1 作业5

 

 

  如上图为第五次作业的复杂度,可以看出,大部分的程序的复杂度都较低,但是请求队列的复杂度,却很高,这里的复杂度很高的原因,是因为这是我第一次使用多线程程序,由于要保证线程安全,在设计请求队列的时候,在请求队列中仅仅使用了一个方法,根据选择的不同参数,来完成不同的公共,从而实验,线程安全,如下:

    public synchronized PersonRequest ChangeRequests(int type,
                                                     PersonRequest e) {
        // type = 1, delete one
        // type = 2, add one
        // type = 0, get size, only return first
        if (type == 0) {
            if (requests.size() == 0) {
                return null;
            } else {
                return requests.get(0);
            }
        } else if (type == 1) {
            PersonRequest personRequest = requests.get(0);
            requests.remove(0);
            return personRequest;
        } else if (type == 2) {
            requests.add(e);
            return null;
        } else {
            return null;
        }
    }

  可以看出,这里的代码虽然不长,但是实现的功能众多,加深了平均循环复杂度。

  类之间的调用关系图如下:

  对类具体分析:

  • Main:程序的初始化,仅调用Schedule进行运行
  • GetInput:获取输入,通过循环实现读取输入
  • Requests:构建需求序列,实验需求序列的读写,上述中将队列的,增、删、查,均写在了一个函数中,使得该类别非常复杂
  • Schedule:该类通过循环查询,是否有增加的需求,和是否退出,教简单
  • Elevator:电梯自身独立,仅考Schedule增加的需求,自身进行执行,教为简单 

  协作图如下:

  按照solid原则,对自己的程序进行分析如下:

  • 单一功能原则:每个类都仅仅执行其相应的功能,这是符合要求的
  • 开闭原则:满足了对修改封闭,部分满足对扩展开放的,其中电梯的执行方法,以及调度器的调度策略对扩展的开放程度低。
  • 替换原则:没有子类,不存在问题
  • 接口隔离原则:没有接口,不存在问题
  • 依赖反转原则:模块化依赖于低层次的模块的抽象,不存在问题

3.2 作业6

 

  从上述的类图分析中,复杂度比较复杂的集中在Elevator类和Schdule类中,因为Schedule需要多次循环的判断状态,而Elevator中也需要多种决策,来判断是否上行,是否下行,以及自己当前的状态,队列的出入等等功能,这部分的复杂度会很高。

  从这张图中也可以看出,Elevator中的复杂度会很好,因为电梯类中,存在有需要的状态,需要维持需要的队列等,这样就使得这个类的复杂度非常的高。

  对类具体分析:

  • Main:程序的初始化,仅调用Schedule进行运行
  • GetInput:获取输入,通过循环实现读取输入
  • Requests:构建需求序列,实验需求序列的读写,上述中将队列的,增、删、查,均写在了一个函数中,使得该类别非常复杂
  • Schedule:该类通过循环查询,是否有增加的需求,但是增加了两个锁,分别与输入类上锁,以及与电梯类上锁,复杂度也随之提高了
  • Elevator:电梯自身独立,仅考Schedule增加的需求,但是自身运行的方式众多,上行、下行、停止,以及需要构造的等待数组,和下电梯的数组,信息众多,导致整个类较长

  协作关系图如下:

  在协作关系上,这部分与第5次作业是一致的,因为第6次作业与第5次作业也仅仅是修改了Elevator的运行的方式,整体上是没有改变的。

  按照solid原则,对自己的程序进行分析如下:

  • 单一功能原则:每个类都仅仅执行其相应的功能,这是符合要求的
  • 开闭原则:满足了对修改封闭,和对扩展开放的,对于电梯的方向的是可扩展的
  • 替换原则:没有子类,不存在问题
  • 接口隔离原则:没有接口,不存在问题
  • 依赖反转原则:模块化依赖于低层次的模块的抽象,不存在问题

3.3 作业7

 

  这里看出,在额外增加了换乘等需求后,复杂度都较大的提升了,在方法的复杂度上,主要是由于Run的复杂度发生了巨大的提高。

按照solid原则,对自己的程序进行分析如下:

  • 单一功能原则:每个类都仅仅执行其相应的功能,这是符合要求的
  • 开闭原则:满足了对修改封闭,和对扩展开放的,对于电梯的方向的是可扩展的
  • 替换原则:没有子类,不存在问题
  • 接口隔离原则:没有接口,不存在问题
  • 依赖反转原则:模块化依赖于低层次的模块的抽象,不存在问题

四:分析自己程序的BUG


  分析自己程序的bug,特征、问题所在的类特别注意分析哪些问题与线程安全相关

  在几次的作业中,都通过了强测,但是在中测环节中,也是发现了一些问题,在此可描述一下中测部分所遇到的问题,以及如何解决的。

第7次作业:

  在第7次作业中始终存在有一个测试点超时,怀疑是出现死锁的现象,导致超时,最终检查出来的问题在与多线程交互过程中产生的死锁

  程序在调度过程中采用的策略为:电梯中关门后,检查上下楼是否有需求,来决定下一步的方向,而此时若调度器传来新的需求,若该需求的起始位置恰好为当前电梯所在楼层,电梯修改下一步方向为0,即既不上升也不下降,此时电梯即停在原地,便产生了死锁。

  在测试的过程中是采用随机生成需求的方法,代码如下:

import os
import random
import time

all = 0
for _ in range(1):
    f = open("test.txt", "w")
    for i in range(random.randint(30, 40)):
        while True:
            From = random.randint(-3, 20)
            if From != 0:
                break
        
        while True:
            To = random.randint(-3, 20)
            if To != 0 and To != From:
                break
        f.write("{}-FROM-{}-TO-{}\n".format(i, From, To))
    f.close()
    g.close()
    begin_time = time.clock()
    os.system("cat test.txt|java -cp .;../../../elevator-input-hw3-1.4-jar-with-dependencies.jar;../../../timable-output-1.1-raw-jar-with-dependencies.jar elevator.Main")
    end_time = time.clock()

    all  += end_time - begin_time

print(all)

  实际中,本次的第7次作业,我也做了一个优化版本,优化版本的思想如下:

  若收到一个需求,该需求需要转乘,将第一阶段需求直接加入到目标电梯中,将第二阶段需求传入转乘电梯,作为一个假需求,当转乘电梯中没有真实需求的时候,便会朝向假需求方向移动,若有真需求,便不会朝向假需求移动,这样的优化可以实现如下情况的优化:

  3-20楼,此时A与C电梯会同时移动,这样会大大节省时间。

  这样的优化结果在整体的随机分布上是否优,以及测试结果上是否正确,能否通过正确性的检查没有得到完善的测试,最后也没有提交该部分的代码,但是目前来看,其实在性能上是能够有占优的。

五:分析其它人程序的BUG


   高工大三,无互测环节,本部分忽略。

 

六:心得体会


  从线程安全和设计原则两个方面来梳理自己在本单元作业中获得的心得体会

  本单元的实验难度是有梯度的,不仅仅在于线程安全的控制上,也在于后续的优化过程中的不同电梯间怎么协调,在多个电梯之间进行协调的时候,不可避免的需要使用某些量进行通信,这样就又增加了分析的难度,如果只是粗暴的加锁,又会降低整个程序运行的性能。

线程安全上:

  线程安全上有多种方式可以保证,轮询,synchronized,加锁,都可以实现,其中轮询的方法结合synchronized不会产生死锁,但是CPU运行时间是急剧上升,这个时候CPU花了大部分时间进行轮询操作,实际上这些操作都是无用的,而加锁使用wait/notify可以很好的解决这个问题,避免CPU长时间进行无效操作,但是这样也不可避免的可能产生死锁的现象,需要严密的思考,来避免死锁。

  在思考如何避免死锁上,可以对需要交互的信息量进行synchronized加锁,这样可以避免对信息量的读写顺序产生的问题。而对于程序逻辑产生死锁,将比较难发现,对于任意没有被原子化的程序段,都需要考虑如果此时切换到其它进程后,会产生怎样的影响,都需要综合考虑,当然这部分也可以被原子化,但是性能上会有所下降,需要综合考虑。

 

设计原则上:

  在本次的实验中还是非常依赖于设计的架构,如何合理的设计自己程序的结构,以及相应的功能由哪些类来实现,如何支持扩展和修改。这些都是需要学习的,目前我的代码这部分仍然有所欠缺,可以再学习一下别人的代码。

 

程序测试上:

  本次多线程的程序测试上,由于不能够debug来测试,变成很困难的一个问题,程序测试主要有大批量随机测试,或者是逻辑分析。前者并不能够保证完全正确,而后者的思维量很大,需要有更精准的测试思路,通过随机测试找到部分问题,再对程序未加锁的程序块进行严格的分析,才能实现较准确的验证。

posted @ 2019-04-24 13:04  Ti-amo  阅读(163)  评论(0编辑  收藏  举报