oo第二次博客

 多线程的协同和同步控制

  1. 第一次作业:总共两个线程,主线程(输入线程)和电梯线程,采用生产者-消费者的设计模式,主线程同样也是输入线程,负责将新的请求加入队列,电梯线程每次取出请求队列的第一个元素。如果队列为空那么电梯线程阻塞,有新的请求加入队列的时候唤醒电梯线程,从而实现线程同步;电梯线程和主线程互斥访问队列。
  2. 第二次作业:方式同上。
  3. 第三次作业:总共四个线程,主线程(输入线程)和三个电梯线程,采用worker-Thread的设计模式,主线程工作方式同上,但是主线程需要监视三个电梯线程,并且维护三个电梯的等待队列,同时当某个电梯线程完成某个乘客的一部分请求需要换乘的时候,将请求扔给主线程。由于每个线程都有自己独立的等待队列,所以线程的同步与互斥仍然采用第一次的作业的设计方式。基于度量

基于度量的程序结构分析

三次作业的设计架构一脉相承,只是调度器的策略略有不同,所以给出第三次作业的分析,来代表这三次作业的结构情况

设计策略

通过一个请求的执行过程来解释上图的结构:

调度器对象是主线程和电梯线程共享的对象(通过单例实现),主线程通过读入请求,调用调度器的put,调度器根据一定的策略将对象放入队列

  1. 第一次作业和第二次作业都是直接将请求放入等待队列
  2. 第三次由于三部电梯的功能不同,调度器中维护了三个等待队列,以及一个共享等待队列。调度器根据请求的楼层来放置请求,如果fromfloor只有某一部电梯可以到达,那么就直接放入这部电梯的等待队列中;如果该请求是一个可直达请求,就放入能直达的且任务数最少的电梯的等待队列中,从而实现负载平衡;其他情况,就放入三个线程的共享等待队列中,让三部电梯来抢夺请求。

第一次中不含有捎带,只有线程的同步互斥,已经做过分析。这里继续来看第二次和第三次的作业中电梯的行为

对于一部电梯而言

  1. 通过调度器的peek方法来获取等待队列第一个乘客的请求,来确定电梯的初始行驶方向(set Direction)
  2. 通过判断请求的运行路线,来确定电梯是只需朝某个方向运行,还是需要折返
  3. 执行tofloor()即沿着设定的方向到每层查看有无乘客上下电梯(hasPassengerIn, hasPassengerOut),如果有就执行(PassengerIn, PassengerOut),先下后上。直到从当前楼层到最远楼层都没有请求(请求方向与电梯的运行方向可同可反)并且电梯中也没有乘客,那么电梯暂时停止。如果在2中确定需要折返就折返,不折反就still,停住就好。这里了仿照扫描算法
  4. 完成一次往返并到达still状态,叫做一次proceed。

第一次作业

类的数据

  • LOC (Lines Of Code – at method and class granularity)

    代码行数,可以看到你的方法和类写了多少行。

  • CC (Cyclomatic Complexity – Method)

    圈复杂度,用于衡量一个模块判定结构的复杂程度,圈复杂度越大说明程序代码质量低,且难以测试和维护。

  • PC (Parameter Count – Method)

    方法中传入的参数个数。

  • NOF (Number of Fields – Class)

    类的字段个数。

  • NOPF (Number of Public Fields – Class)

    类的公共(public)型字段个数。

  • NOM (Number of Methods – Class)

    类的方法个数。

  • NOPM (Number of Public Methods – Class)

    类的(public)型方法个数。

  • WMC (Weighted Methods per Class – Class)

    类的加权方法个数。具体加权算法怎么算,笔者不太清楚。

  • NC (Number of Children – Class)

    类的子类个数。

  • DIT (Depth of Inheritance Tree – Class)

    类所在的继承树深度。

  • LCOM (Lack of Cohesion in Methods – Class)

    方法的内聚缺乏度。值越大,说明类内聚合度越小。

 方法度量

 

 

 代码异味与优缺点

缺点

电梯的上行与下行的时间这里我直接写的,导致Magic Number,下次需要改进;内聚不够

优点

代码行数均衡,复杂度适中

UML时序图

第一次就是生产者消费者,时序与第二次相同,只是不含有捎带,可以参见第二次的UML

SOLID设计检查

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

OCP:不符合,由于第一次的情况较为简单,所以队列直接使用了BlockingQueue

LSP:没有使用继承,无法判断。

ISP:没有使用接口,无法判断。

DIP:不符合,由于第一次作业每次只上一个人,直接get就好,但是之后几次作业中都是要求返回队列中的一些元素,所以需要改变参数和返回值

 第二次作业

类的数据

方法的数据

 

Direction是一个辅助类,用于计算某个特定方向上的距离,以及根据起点和终点判断方向,等一些和方向有关的辅助方法

代码异味与优缺点

 

同第一次作业,电梯的运行时间我直接写的,peek涉及到队列中的元素状态判断所以复杂度较高

缺点

电梯的上行与下行的时间这里我直接写的,导致Magic Number,下次需要改进;内聚不够

优点

代码行数均衡,整体复杂度适中

UML时序图

SOLID设计检查

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

OCP:符合,第二次和第三次都采用了相同的队列设计方式

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

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

DIP:符合,电梯使用调度器的函数在第二次和第三次中没有发生变化,所以只是调度器内部的策略以及函数发生变化,所以是符合DIP准则的

 第三次作业

类的数据

方法的数据

代码异味与优缺点

 同第一次作业,电梯的运行时间我直接写的,get涉及到队列中的元素状态判断所以复杂度较高

缺点

电梯的上行与下行的时间这里我直接写的,导致Magic Number,由于不同电梯的停靠楼层不同所以有几个MagicNumber,下次需要改进;内聚不够

判断是否是孤立楼层我是遍历三部电梯,其实可以一开始就写好这个函数,每次都遍历效率很低

判断是否有乘客需要判断三部电梯的状态这里的判断也比较高,所以复杂度较高,考虑拆成小的函数来处理

优点

代码行数均衡,整体复杂度适中

UML时序图

SOLID设计检查

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

OCP:不符合,由于我根据请求能否直达以及是否处于孤立楼层来拆分任务,所以原来的put函数就需要修改了,如果需要继续扩展策略就需要继续修改put函数

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

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

DIP:与第二次情况相同,符合

BUG分析

第一次作业

由于我使用了BlockingQueue自带isEmpty方法导致我在检查队列是否为空和之后的操作产生了间隙,从而导致了check-then-act无法实现。这里的设计要求以后尽量将这种行为做到原子,在不同的情况下需要考虑封装好的数据结构是否可以满足多线程的原子操作方式。

第二次作业

  1. 在标准输入读入结束标志的时候,忘记唤醒阻塞的电梯的进程,导致其一直等待。这里需要注意分析线程唤醒的各种情况,保证在需要唤醒的时候notify
  2. 由于忘记使用wait和notify导致cpu_time超时,变成了循环判断,而不是判断不成立就阻塞

第三次作业

  1. 由于每个电梯都要共享同一个调度器实例,我将电梯里面的调度器实例的初始化放到了电梯的初始化函数中,而调度器在初始化的时候会初始化三个空的电梯,这样就导致互相初始化,最终导致电梯中的调度器是一个空值
  2. 判断电梯进程结束的标志,自己的请求队列是否为空,在电梯结束的时候,唤醒其他阻塞进程,最终导致其他电梯的捎带乘客在扔给这部电梯时,这部电梯早已退出了

这里需要注意面向对象的初始化顺序,以及多个线程相互协作的情况,前两次由于电梯只有一部所以较为简单,但是多部电梯的时候,不仅要考虑生产者和消费者的关系,已及每部电梯自己结束的标志,还需要全局考虑其他的电梯(线程)。

心得体会

线程安全:当出现多个线程都需要访问同一个变量的时候,并且有某个线程要修改这个变量的时候,就需要考虑对这个变量上锁,即要求临界区的代码只能有一个线程在执行。同时还要注意临界区的大小,以及check-act的标准

设计原则:尽量按照某种以有的设计模式,或者可以规约到某种设计模式来进行设计多线程的运行方式,出现多个锁的时候注意预防死锁,以及死锁的消除

 

posted @ 2019-04-24 10:43  王延鹏  阅读(168)  评论(0编辑  收藏  举报