电梯模拟系统——BUAA OO第二单元做业总结

需求分析

官方需求

  • 本次做业须要模拟一个多线程实时多电梯系统,从标准输入中输入请求信息,程序进行接收和处理,模拟电梯运行,将必要的运行信息经过输出接口进行输出。
  • 本次做业电梯系统具备的功能为:上下行,开关门。本次多部电梯的可停靠楼层,运行时间,最大载客量都不相同。
  • 电梯系统能够采用任意的调度策略,即上行仍是下行,是否在某层开关门,均可自定义,只要保证在系统限制时间内将全部的乘客送至目的地便可。
  • 电梯系统在某一层开关门时间内能够上下乘客,开关门的边界时间均可以上下乘客。

简要分析

此次做业有几个关键点:实时系统,正确调度,多线程交互。这就要求在设计中须要提早定义好运行逻辑,并且经历了第一单元的历练,在第一次做业就须要对后续做业可能出现的状况进行设计——即尽可能最大化程序的可拓展性,下降程序模块之间的耦合程度,这样在需求变化时就能尽可能少的修改代码达到需求。<br>本次做业三个阶段性安排为:java

  • 第一阶段: 多线程单部电梯,先来先服务原则运行(即电梯一次执行一个任务),楼层号连续
  • 第二阶段: 多线程单部电梯,可捎带策略运行(即电梯中容许携带多人,无最大人数限制),包括地上(正数)和地下楼层(负数)【要求使用wait/notify机制,不能使用进程轮询,下同】
  • 第三阶段: 多线程多部电梯,可捎带策略运行,电梯有最大人数限制,电梯可停靠楼层、运行时间有不一样限制

本单元做业输入输出接口均已给出,所以重点就在于程序进程交互逻辑以及任务分配执行的算法。python

逻辑设计

由于有了第一单元设计的经验,为了减小每次的修改量,在每次做业的实现中都尽可能采用逻辑分离式设计,每一个模块独立的从队列中取出信息、处理信息、放置信息(反馈)。模块之间只经过队列交互,而后独立的实现一套逻辑。针对各个部分,设计变化以下:git

  • 第一阶段: 直接按照推荐的模式将需求和任务进行了分离,即线程交互分为两个过程:
    • 第一过程使用单独的线程处理输入,并建立请求队列,由Submission和Scheduler交互访问——Submission读入输入放置请求,Scheduler取出请求布置任务。
    • 第二过程由Scheduler与Elevator进行交互,共享Mission队列与ElevatorState电梯状态板——Scheduler取出请求后读取状态板,分配任务给电梯(由于只有一部,因此这个阶段并无具体实现分配算法函数,只是返回当前电梯名);电梯根据任务队列执行任务,运行时主动更新状态板信息。
    • 为了保持多电梯的扩展性,我在Scheduler中将可调度电梯设置成了“注册”模式,即主线程建立电梯对象后,调用addElevator(Elevator ele)函数添加一部电梯使得电梯可被调度。
  • 第二阶段: 第二阶段除将轮询交互改成wait/notify机制以外,主要增长的需求为捎带和负数楼层且-1->1的楼层变化并不连续。
    • 针对楼层需求,我设置了有序列表使得楼层之间的变化保持连续性:github

      List list = Arrays.asList(-3,-2,-1,
          1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20);

      这样在程序中修改楼层因此便可实现楼层之间的连续变化。算法

    • 针对捎带需求,在个人设计中将可捎带断定与捎带执行分配到了电梯本身的线程中,所以这部分基本只对Elevator类进行了修改,在电梯开门、关门、变化楼层、任务开始时遍历Mission队列决定可捎带的任务并判断下一个停靠的目标楼层,以适应不断到来的任务变化。设计模式

    • 须要注意的是,此次由于捎带的发生,须要进行Mission状态的记录——即人在电梯内或者电梯外->进一步为了简化逻辑,将这个状态的判断转化为“Mission在执行队列或等待队列中”。这样作的好处是能够将上下电梯统一块儿来,基于上下电梯须要的电梯行为表现(到达-开门-关门)是统一的这点,可使得电梯逻辑变得简单,至于上或下只须要体如今输出便可。安全

  • 第三阶段: 第三阶段实现了真正的多电梯交互,并且提升了楼层不连续性的限制,并对电梯人数进行了限制,这些相应的限制实际上是对调度器提出了需求;除此以外,换乘状况的增长我直接采用了任务分解处理。同上,这个单元依旧不须要更高Submission模块以及其与Scheduler的交互,甚至Scheduler与Elevator的交互逻辑也不须要更改,只须要更改各自的处理算法便可。
    • Scheduler中须要修改的就是以前预留的String determine(Mission request)函数
      /**
      * 考虑:电梯运行状态+电梯选项+目标电梯
      *      输入Mission:目标换乘楼层(须要换乘时)
      *      1. 若是选择只有一个,直接上
      *      2. 若是选择有多个,基于“换乘点是一致的”的条件
      *         分支树断定(方向、捎带、远近、人多少)
      * @param request
      * @return 目标电梯名
      */
      private String determine(Mission request)
      另外,由于不一样的电梯线程都须要访问调度器的部分方法,将Scheduler做为了Main类中的静态实例处理,不知道算不算单例的一种实现,但由于Scheduler中保证了线程安全,并且只在主线程中实例化,所以这个静态实例变量能够正确使用
    • ElevatorElevatorState两部分配合修改实现了楼层可达性控制‘’‘’‘’‘及Mission分阶段执行。在原来楼层的list的基础上在构造方法中增长了
      this.availableFloor = new ArrayList<>();
      this.availableFloor.addAll(floors);
      来断定可停靠的楼层,至于具体是哪些,在建立电梯时指定,电梯中还增长了相应的接口进行楼层判断。实现整套逻辑。
    • Mission部分为了利用现有的逻辑完成换乘设计,将请求按照换乘须要拆分红了不一样的Mission阶段,request与Mission扔保持一一对应关系来保证乘客不会出现“分身”的状况,执行完一个阶段由电梯将此Mission交还给Scheduler从新参与调度。所以,在电梯状态板增长了“是否可达的”断定:
      public Boolean canDeliver(int floor) {
          if (!this.availableFloor.contains(floor)) {
              return false;
          }
          return true;
      }
      相应的在Scheduler中提供了函数判断是否可直达
    • 电梯任务分配算法可表示为:
      /**
      * 可直达? |--yes--> 使用直达电梯
      *         |--no---> 拆分红两阶段
      *
      * 当前阶段性任务有多部电梯可完成:1.当前方向上可捎带? ---> 2.电梯中人数较少?
      */
      这样就可以较为合理地规划电梯的任务分配并处理好换乘状况。

最终获得的总体逻辑的时序图以下:多线程

时序图

BUG分析

本次做业第一阶段和第三阶段都未发现bug,第二阶段强测中发现了一个bug,主要是由于第二阶段对调度性能要求较高,可捎带的状况必需要捎带上才行,而我在设计Mission队列以及Elevator正在执行的Mission存储两部分同步不是很好,致使Mission队列中有任务,却由于检查不及时Elevator没有检查并执行这个任务,尤为是某一层有不少人上电梯时(同时有不少人同一楼层的用户请求到来)。这个bug的出现和输入的时机有关,所以本身测试的时候没有检测到。<br> 第三阶段我花费了很长时间在debug上,由于评测机一只返回RUNNTIME_ERROR,但个人程序并不会出错,为此,我还实现了简单地随机测试脚本:架构

import random
pair = []
l = [-3, -2, -1, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]
for i in l:
	for j in l:
		if i == j:
			continue
		else:
			pair.append((i,j))
n = 0
time = 0.0
with open("data.txt", 'a') as f:
	for t in random.sample(pair, 50):
	    print("[{:.1f}]{}-FROM-{}-TO-{}".format(time, n, t[0], t[1]))
	    status = f.write("[{:.1f}]{}-FROM-{}-TO-{}\n".format(time, n, t[0], t[1]))
	    n += 1
	    if random.random() > 0.6:
	    	time += random.random()
	f.write("\n")

以上使用python自动生成数据,借助Mistariano提供的黑箱接口能够进行简单地测试,数据生成能够经过调节参数控制app

@echo off
set num=0
:start
set /a num+=1
echo ================No. %num% time======================= >> err.txt
python gen.py | "C:\Program Files\Java\jdk-11.0.2\bin\java.exe" -cp out\production\oo_course_e_2019_16071064_homework_6;..\..\elevator-test-suit-0-3.jar Main >> err.txt 2>&1
echo ================    over      ======================= >> err.txt
goto start

虽然使用了大量的黑箱测试个人代码却依然有RUNTIME_ERROR的问题,因而我又尝试其余的debug,终于发现是由于程序退出时Scheduler发生了轮询致使CPU时间超时(这一点评测机并无反馈,并且错误种类不该该是这个啊),这里,JProfile的线程分析帮了我大忙

JProfile分析结果

这个界面显示了每一个线程实际运行的时间,若是有线程使用轮询致使CPU时间过程能够明显的看出并直接定位。

代码分析

类图

相比较以前的设计,这个单元我认为做为比较好的是在第一次代码就肯定了整个的设计框架,也证实在新需求到来时能够较快的适应改动。

类图

从图中能够看出各个类的功能划分是比较清晰地,关于共享对象的设计以及线程类的实现是和预期设计一致的。并且每个线程类都不依靠其余线程,是一个独立的逻辑体,这大幅下降了模块之间的耦合性,也使得代码逻辑更为清晰易懂。

度量分析

  • 代码量分析

    代码量分析

    此次的代码相比较第一单元有了很大的改观,首先是类的代码量,大部分功能类的代码量分布仍是比较均衡的,代码量最多的类源码没有超过300行,并且在完成过程当中我也尝试使用Javadoc风格的函数与类功能注释,注释比例有了很大的增长。重要的是,使用这样的注释以后在下一个阶段须要更新代码的时候可以很快的回忆起函数的功能与实现。<br>此次类间代码量的均衡得益于功能上的分工明确,即将不一样的功能实现交由不一样的模块完成,但其中由于电梯的运行策略最为复杂,代码行数较多。但总体上仍是比较合理的。

  • 方法复杂度分析 此次代码的复杂度分析呈现出如下结果,共出现了三个复杂的方法,一个复杂的条件判断,一个长参数列表问题,这些问题的应该是能够解决的。总体来看,代码的循环复杂性(CC)为2.49122807,其中循环复杂性最高的函数依旧是iterMission,高达13。这个函数的功能是遍历Mission队列并添加Mission到电梯的执行队列中,从这一点来看,这个函数仍是有能够简化的空间的。

Class Name Method Name Code Smell
Elevator Elevator Long Parameter List
Elevator checkDirection Complex Method
Elevator iterMission Complex Method
Scheduler run Complex Method
Scheduler determine Complex Conditional
  • 类复杂度分析 总体来看,类之间仍是比较均衡的,和最初的设计一致,只有Main和Submission较为简单,这两个类自己也没有什么复杂的逻辑也不须要完成什么实质性的算法功能,所以仍是比较合理的。
Type Name NOF NOPF NOM NOPM LOC WMC
域数量 public域数量 方法数量 public方法数量 行数 类加权方法数
Elevator 8 0 15 4 302 58
Main 1 0 3 3 37 4
Mission 6 0 10 9 118 17
Scheduler 5 0 11 7 200 33
Submission 2 0 2 2 46 4
ElevatorState 9 0 16 16 121 26

总结与感悟

这是我第一次接触多线程程序,以前只是在学习操做系统的时候了解过原理,但并无动手写过多线程程序。经过此次实践我不只掌握了多线程程序的设计方法,并且了解到设计模式在多线程交互中的关键性。不管是线程安全仍是死锁预防,或者是线程之间wait/notify的等待唤醒机制,都让我对线程交互有了更深层的理解,并且认识到使用JProfile分析程序运行状态进行优化的必要性。<br>真正测试代码时,白盒测试是比较直接的方式,但黑盒测试优点也能提供意想不到的效果与便捷性,并且从某种程度上讲更能达到压力测试的效果。<br>另外,在设计之初就考虑可扩展性是颇有必要的,一套好的架构必定是可以支持不断扩展的架构,进行好架构设计对功能扩展不管是工做量仍是安全性都有很好的帮助。

相关文章
相关标签/搜索