zoukankan      html  css  js  c++  java
  • OO第二单元作业总结

    一、第一次作业——FAFS调度

    1.设计策略

    第一次作业要求模拟单部电梯的FAFS傻瓜调度,我采用共享对象实现多线程的同步控制。程序由输入线程和电梯线程两个线程组成,其中,输入线程作为主线程。采用了生产者-消费者模式,将调度器作为共享对象(类似于生产者-消费者模式中的托盘),并装有请求队列,输入线程(生产者)每次读到一个输入,就将其加入到调度器的请求队列中,电梯线程(消费者)从调度器中拿请求,并处理请求,对调度器中涉及请求队列的方法加锁以保证线程安全。

    2.基于度量分析程序结构

    2.1 结构分析

    在进行分析之前,先解释一下以下几个缩写:

    LOC:代码行数

    CC:圈复杂度,反映了方法控制分支的数量,越高意味着代码越可能质量低且难以测试、维护。

    PC:方法参数个数

    NOF:类的属性个数

    NOPF:类的public属性个数

    NOM:类的方法个数

    NOPM:类的public方法个数

    WMC:类的加权方法数

    NC:类的子类个数

    DIT:类的继承树深度

    LCOM:类中内聚度的缺乏,越大意味着内聚度越差。

    FAN-OUT:某个类引用其他类的次数

    FAN-IN:类被其他类引用的次数

    程序的基本结构度量分析如下:

    可见,本次作业各个类都比较简单,代码规模小,且方法的控制分支数目少,实现较为简单。

    2.2 类图

    这三个类的功能较为独立,MainClass是输入主线程,负责接收输入,并将输入插入到请求队列中。Elevator负责从请求队列中取出请求,并执行请求。调度器则提供了setRequestList和getRequestList方法供生产者和消费者使用。这种设计比较简单,每个类的功能比较独立,通过共享请求队列实现同步控制。

    2.3 UML协作图

    2.4 SOLID设计检查

    SRP:符合,每个类和方法功能单一。

    OCP:不符合,如果要将电梯扩展成ALS调度,则需要修改调度器中请求队列的结构,而无法直接使用原有的请求队列,而请求队列结构的改写会造成相关方法的改写,修改的幅度较大。

    LSP:由于没有使用继承,所以没有体现这一点。

    ISP:由于没有使用接口,所以没有体现这一点。

    DIP:不符合,Elevator类和MainClass类都依赖于Scheduler类,调用了其提供的一些方法,因此如果Scheduler对外提供的接口发生了改动,则ElevatorMainClass也有可能需要改动。

    3. BUG分析

    刚开始写电梯的时候,遇到的第一个bug就是——这个程序要怎么停下来啊,后来我在调度器里设置了一个标志END来表明输入线程是否已经结束,一旦输入线程结束,就会调用调度器的SETEND方法,将调度器中的END置位,告诉调度器:嘿,我没输入了。然而,这次不仅没有停下来,电梯还唤不醒了,后来发现是因为在调度器这个共享对象的getRequestList方法中,我写的是当END没有置位并且请求队列为空时,电梯线程进入等待状态,而setRequestList中,输入线程将一个请求加入到队列中时,会使用notifyall唤醒正在等待的电梯线程。这样听起来好像是,没啥问题啊,然而电梯线程wait的条件中还有一个“END没有置位”,这意味着,当END置位了以后,也需要使用notifyall唤醒电梯线程,防止明明END置位了,但是电梯仍然在等待的情况。

    二、第二次作业——ALS调度

    1. 设计策略

    第二次作业在第一次作业的基础上,对调度方法进行了改动,模拟单部电梯的ALS调度。为了能够较快地判断某一楼层是否有需要进入电梯的请求以及电梯内部是否有在这一层出去的顾客,我进行了重构,对调度器的请求队列进行了改进。在上一次作业中,请求队列较为简单,仅仅是按照请求到达的顺序构造的。在本次作业中,我在调度器中模拟了一个-3~16层楼的结构,请求队列是一个数组,每一项代表要在这一层进入电梯的请求。除此之外,为了能够支持捎带,我在电梯内部也构造了一个类似的队列,每一项代表要从这一层出电梯的顾客。

    程序整体的架构同第一次没有太大的区别,依旧采用生产者-消费者模式:主线程是输入线程,负责将请求插入到调度器中的请求队列中;电梯线程从调度器中获得请求,并将其加入到自己的请求队列中,执行请求队列中的全部请求。其中,电梯的内部调度有一点不同的是,电梯每次从调度器中获得一个主请求,这个主请求决定了电梯运行的方向,在前往主请求楼层的过程中,电梯会不断捎带请求,并将得到的请求加入到自身的请求队列中,在电梯下一次获得主请求之前,电梯内部的请求队列被完全执行完了。因此,电梯除了会从调度器中获取主请求之外,还应该能够监控到调度器请求队列的状态,得知每一楼层当前是否有进入的请求。此外,我所实现的电梯只能支持同方向的捎带,这要求调度器中请求队列的每一项除了能够表示从该楼层进来的请求之外,还要能区分请求的方向,因此每一项里会有updown两个链表,表示了向上和向下执行的请求。

    此外,在取得主请求的时候,每次取出的是到达最早的请求,由于调度器中的请求队列模拟了楼层,因此失去了请求到达顺序这一特点(在第一次作业的请求队列中,队首的请求一定是到达时间最早的请求),因此,我在请求类中增加了order属性,这个属性在输入线程中获得,每次从控制台得到一个新请求,order++,用于标识请求的到达顺序。因此,请求类从第一次作业的(id,from,to)变为了(id,from,to,order)

    2. 基于度量分析程序结构

    2.1结构分析

    程序的基本结构度量分析如下:

    为了支持模拟楼层的请求队列,这次作业涉及的类比较多。可见,Scheduler代码的长度有明显的增长,SchedulerElevator类的圈复杂度也有所增加,控制分支的数目增多,这都给代码的调试带来了一定程度的困难,增加了代码的复杂性。类的加权方法数明显增多,这意味着一个类中包含了多个方法,可能造成类的功能多样,不符合类功能单一的设计原则。

    2.2 类图

    可见,类的调用关系比较复杂,且每个类中的属性和方法数目比较多,主要的类(例如:Scheduler、Elevator)比较庞大。ALS捎带的调度算法是在电梯类中实现的,导致电梯的功能比较复杂,需要实现获得请求+处理请求两个操作,可以将处理请求从电梯中分离,单独用一个类负责请求的执行,这个类可以作为电梯的局部调度器,具体实现ALS调度算法。与电梯类相比,调度器类的功能比较单一,主要实现add请求+get请求操作,维护线程安全的请求队列,因此,调度器类功能的划分还是比较合理的。

    2.3 UML协作图

    2.4 SOLID设计检查

    SRP:不符合,电梯类功能过于复杂,包括获取请求和ALS调度处理请求两种功能,可以考虑将ALS调度功能从电梯类中分离出来,作为局部的调度器对电梯中的请求进行处理。

    OCP:符合,在第三次作业的实现时,为了能够支持多部电梯的ALS调度,我在第二次作业上进行了扩展,增加了调度器的功能,电梯类基本保持不变,没有进行大规模的改写。

    LSP:没有使用继承,因此无法体现。

    ISP:没有使用接口,因此无法体现

    DIP:不符合,仍然出现了高层模块依赖于底层模块的现象。例如,电梯类和主类都依赖于调度器类,这两个类中都出现了对调度器的引用,并通过使用调度器提供的方法实现自身的功能,因此,如果调度器的方法进行了修改,则可能会引起其他两个类的修改。

    三、第三次作业——智能调度

    1. 设计策略

    第三次作业要求模拟多部智能电梯,并且还存在转乘的情况,我还是采用了ALS捎带调度算法。

    由于三部电梯的速度、客载量、名字、到达楼层都不同,因此可以使用工厂模式,在构造电梯对象时对于这些属性赋值,得到不同属性的电梯。

    这次采用了两层调度的策略,调度器由主调度器与副调度器构成。第一层调度由主调度器线程完成,主调度器中维护一个线程安全的请求队列,这个队列就是一个简单的队列(与第一次作业一样,没有模拟楼层),主调度器线程根据自己的调度算法负责将请求分给三个副调度器。

    主调度器的调度算法如下:收到一个请求,先判断是否需要转乘,如果需要换乘,则记录这个请求的中间换乘层,并将请求修改为换乘的前半段,否则,不做处理;对于处理好的请求(原请求或换乘请求的前半段),找到可以执行这个请求的电梯集合;对于电梯集合里的电梯,如果当前有电梯可以捎带这个请求,则选择空闲人数最多的电梯,尽量使得电梯可以捎带上这个请求,如果都不能捎带这个请求,那么就选择更早到达请求from层的电梯,减少请求的等待时间。

    副调度器则分别对应一个电梯,每个副调度器中也都有一个请求队列,这个请求队列同第二次作业一样,副调度器实际上是主调度器线程和电梯之间交互的共享对象,主调度器线程将请求分发给三个副调度器,三个电梯分别从自己所对应的副调度器中获得请求,并按照ALS调度进行处理,副调度器和电梯相当于第二次作业中的调度器和电梯。

    通过两层调度器,将请求分发给不同的电梯和电梯执行请求这两部分分离开,因此,电梯在执行请求的时候不需要考虑这个请求的楼层是否自己能到达,该项请求是否能成功执行,只需要从副调度器中不停获取并无脑执行即可,这是因为,在主调度器分发请求的时候,已经根据各个电梯的可到达楼层和速度等因素,将请求分配给了不同电梯,保证了电梯所分配到的请求一定是其能正常执行的。

    除了分配电梯之外,如何处理换乘也是需要注意的问题。首先,由三个电梯可到达的楼层可以看出,请求至多需要一次换乘。需要解决的问题主要由以下两个:怎么找换乘的中间站;怎么保证换乘的第二段请求在第一段请求之后执行。

    • 对于第一个问题,我采用的方法是,对于每一个请求,设置两个集合fromSettoSet,分别存储可以到达from层和to层的电梯,求出这两个集合之后,求temp = fromSet & toSet,即两个集合的交集。如果temp不为空,表明这个请求有可以直达的电梯,则分给直达电梯;否则,则表明需要换乘。对于换乘的情况,对于fromSettoSet中的任意两个电梯的组合,分别向上行方向和下行方向查找第一个两电梯都可以停靠的楼层,并记录换乘上下行所花费的时间,选择消耗时间最小的一种换乘方法。这里,需要说明一下,需要上行、下行都要查找的原因是,对于2->3这种请求,需要分解成2->1+1->3这种换乘方法,因此可以得知,换乘并不是单纯地由两段同方向的请求构成,也有可能出现两段相反方向请求的情况,即,如果想要上楼,可能要先下楼再上楼。
    • 对于第二个问题,我将这个任务交给了电梯。与前两次作业不同的是,在请求类中,我新增了一个属性,用于处理换乘的情况。在第二次作业中,请求类仅包含(id,from,to,order),现在变成了(id,from,to,finalto,order),其中finalto表示这个乘客最终到达的楼层,to表示这个请求所要到达的楼层,因此,对于换乘请求而言,tofinalto是不相等的。根据这个特点,电梯在将请求送出电梯时可以通过检查tofinalto是否相等来判断是否会产生一个新的请求,这个请求就是原换乘请求的后半段。为了方便说明,下面给出换乘请求的时序图,其中,换乘请求R被拆分成R1R2两个请求:

    使用以上这种方法,由于换乘的后半段请求是在前半段请求执行完之后产生的,因此,避免了换乘请求执行顺序所产生的错误。

    除了两层调度器之外,依旧是主线程作为输入线程,将从控制台得到的请求插入到主调度器的请求队列中,电梯线程从副调度器获得请求,并使用ALS调度算法执行请求。

    2. 基于度量分析的程序结构

    2.1 UML协作图

    为了能更好的理解前面“设计策略”中的内容,先给出各线程的协作图

    2.2 结构分析

    程序基本的度量情况如下:

     

    可见,本次作业的程序明显复杂了很多,无论是代码规模还是圈复杂度都有了较大幅度的增长。其中,主调度器类和副调度器类的代码规模尤其大,由于主调度器中有关将请求分配给不同电梯的部分所涉及的算法比较繁杂,因此,主调度器中这部分所占据的规模比较大。对于副调度器而言,整体规模与第二次相比差不多。

    除此之外,电梯类和主调度器类的加权方法数比较高,这表明这两个类的功能比较复杂,可能违背了类单一功能的原则,其中,电梯类负责取出请求+ALS算法执行请求,主调度器类负责管理内部请求队列+分配请求。

    2.3 类图

    可见,各个类的调用关系非常复杂。将副调度器作为共享对象使得主调度器线程和电梯线程可以直接交互,这一点符合线程安全的要求。但是,输入线程和主调度器线程、电梯线程和主调度器线程的交互都是直接进行的,并没有经过中间的共享对象进行过渡,这是一种比较危险的行为,容易产生线程安全问题,因此在加锁的时候需要特别注意,防止遗漏,或者新构造一个类作为共享对象用于两个线程之间的交互。

    2.4 SOLID设计检查

    SRP:不符合,电梯类有获得请求和ALS调度执行请求两个功能,可以构造一个类专门负责ALS调度,即电梯的局部调度。此外,主调度器的功能也过于复杂,包含维护请求队列和分配电梯请求两个功能,由上述的度量结果图可以看出,主调度器的代码规模大且控制分支多。

    OCP:符合,如果再增加电梯的数目,则可以直接扩展主调度器中的方法,无需改写。

    LSP:未使用继承,无法体现。

    ISP:未使用继承,无法体现

    DIP:不符合,输入线程、电梯线程都依赖于主调度器和副调度器,都通过获得对他们的引用来调用这两个类对外提供的方法。

    3. BUG分析

    第三次的BUG主要出现在两个位置:程序怎么停下来;请求队列怎么保证线程安全。

    • 首先,第一个问题,尽管在第一次作业中遇到过,但是,到了第三次作业,我还在思考这个问题。在前两次作业中,可以通过在调度器中添加一个END标志表示输入进程是否结束来实现,但是,这次作业中,情况又不一样了。前两次作业中,一旦输入进程停下来了,并且请求队列空了,就意味着请求全部被处理完了,程序可以结束了;然而,这次,由于换乘的原因,即使某一个时刻输入结束了,主调度器的请求队列空了,但并不意味着,请求全部处理完了,因此可能某些换乘的后半段请求还没有产生,如果这时就让程序结束,会导致部分换乘请求无法得到正确的处理。后来,我使用计数的方法解决了这个问题。在主调度器中维护两个变量:inRequestoutRequest,分别代表应该处理的请求数目和已经分配给电梯的请求数目。输入线程每加入一个请求,inRequest++,主调度器每从请求队列中取出一个请求,outRequest++,如果发现这个请求需要换乘时,inRequest++。这样,当输入线程结束,inRequest==outRequest时,如果主调度器的队列为空,则表示所有的请求(包括换乘的后半段请求)都已经分配给了各个电梯,主调度器线程可以结束了。
    • 关于第二个问题,由于主调度器的分配算法的原因,需要去读取电梯中的某些变量,因此,我在电梯中对这些变量给出了对外的GETSET函数,一开始的时候我只将SET函数加了锁,后来发现主调度器得到了错误的数值。这让我意识到我忘记了处理读写的互斥问题,即当电梯线程在修改这些变量的时候,其他的线程不能读这些变量,因此,GET函数也需要加锁。

    四、测试方法

    • 编写测试文件test.java程序,使用Thread.sleep和System.out.println实现在某一时刻输出请求。
    • 在IDEA的Terminal中使用管道将源程序的输入重定向为测试文件的输出,从而实现在某一时刻向源程序投放一定量的请求这一功能。

    五、心得体会

    这三次作业让我对实现线程安全有了比较深入的了解,在做程序设计的时候,需要了解每一个线程会使用的对象,列出可能会存在线程共享的对象,并对他们通过加锁来实现线程安全。

    此外,这三次作业都没有完完全全符合SOLID的设计原则,虽然不遵循设计原则也照样能实现相应的功能,但是在将来的功能扩展中可能会遇到问题,使得程序的可扩展性不强,甚至出现大规模改写以及重构的情况。例如,我的第一次作业可扩展性就不强,导致我第二次作业几乎是重新写了一份,但是第二次作业的可扩展性就比较好,因此第三次作业虽然任务量比较大,但是几乎都是在原有功能上的扩展,改写的部分比较少。

     

     

  • 相关阅读:
    flex + bison multiple parsers
    Educational Codeforces Round 95 (Rated for Div. 2)
    python学习笔记 day20 序列化模块(二)
    python学习笔记 day20 常用模块(六)
    python 学习笔记 常用模块(五)
    python学习笔记 day19 常用模块(四)
    python学习笔记 day19 常用模块(三)
    python学习笔记 day19 常用模块(二)
    python学习笔记 day19 作业讲解-使用正则表达式实现计算器
    python学习笔记 day19 常用模块
  • 原文地址:https://www.cnblogs.com/qrrr/p/10745583.html
Copyright © 2011-2022 走看看