需求分析

官方需求

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

简要分析

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

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

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

逻辑设计

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

  • 第一阶段: 直接按照推荐的模式将需求和任务进行了分离,即线程交互分为两个过程:

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

      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的出现和输入的时机有关,因此自己测试的时候没有检测到。

第三阶段我花费了很长时间在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提供的黑箱接口可以进行简单地测试,数据生成可以通过调节参数控制

@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的线程分析帮了我大忙

这个界面显示了每个线程实际运行的时间,如果有线程使用轮询导致CPU时间过程可以明显的看出并直接定位。

代码分析

类图

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

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

度量分析

  • 代码量分析

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

  • 方法复杂度分析

    这次代码的复杂度分析呈现出以下结果,共出现了三个复杂的方法,一个复杂的条件判断,一个长参数列表问题,这些问题的应该是可以解决的。整体来看,代码的循环复杂性(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分析程序运行状态进行优化的必要性。
真正测试代码时,白盒测试是比较直接的方式,但黑盒测试优势也能提供意想不到的效果与便捷性,而且从某种程度上讲更能达到压力测试的效果。
另外,在设计之初就考虑可扩展性是很有必要的,一套好的架构一定是能够支持不断扩展的架构,进行好架构设计对功能扩展无论是工作量还是安全性都有很好的帮助。

电梯模拟系统——BUAA OO第二单元作业总结的更多相关文章

  1. 你电梯没了—OO第二单元作业思考

    写在前面 这三次电梯调度作业,主要是学习多线程并行操作,对于各个线程的时间轴的把握,互相的配合与影响,通过使用锁来解决访问冲突等方面. 个人在学会Thread相关操作之外,写出来一些奇怪结构的诡异操作 ...

  2. 电梯也能无为而治——oo第二单元作业总结

    oo第二单元作业总结 一.设计策略与质量分析 第一次作业 设计策略 在第一次作业之前,我首先确定了生产者--消费者模式的大体架构,即由输入线程(可与主线程合并)充当生产者,电梯线程充当消费者,二者不直 ...

  3. 【OO学习】OO第二单元作业总结

    OO第二单元作业总结 在第二单元作业中,我们通过多线程的手段实现了电梯调度,前两次作业是单电梯调度,第三次作业是多电梯调度.这个单元中的性能分要求是完成所有请求的时间最短,因此在简单实现电梯调度的基础 ...

  4. oo第二单元作业总结

    oo第二单元博客总结 在第一单元求导结束后,迎来了第二单元的多线程电梯的问题,在本单元前两次作业中个人主要应用两个线程,采用“生产者-消费者”模式和共享数据变量的方式解决问题.在第三次作业中加入多个电 ...

  5. OO第二单元作业总结【自我反思与审视】

    第二单元作业的完成史,就是一部心酸的血泪史…… 多线程的出现为我(们)打开一片广阔的天地,我也在这方天地摸爬滚打,不断成长!如果说第一单元之前还对Java语法有所了解的话,那么这单元学习多线程则完全是 ...

  6. OO第二单元作业——魔鬼电梯

    简介 本单元作业分为三次 第一次作业:第一次作业要实现单部简单电梯,停靠所有楼层,无载客容量,性能分考量电梯运行时间. 第二次作业: 第二次作业实现多部电梯,电梯数量由初始化设定,每部电梯都停靠所有楼 ...

  7. OO第二单元作业小结

    前言 转眼已是第九周,第二单元的电梯系列作业已经结束,终于体验了一番多线程电梯之旅. 第一次作业是单电梯的傻瓜调度,虽然是第一次写多线程,但在课程PPT的指引下,写起来还是非常容易:第二次作业是单电梯 ...

  8. OO第二单元作业分析

    前言 这一单元关于线程安全的作业结束了,在助教提供的接口的帮助以及老师提供的设计模型的指导下,这三次作业还是相对轻松地完成了,中间也没有出现什么bug,可能就是因为简单的逻辑不容易出错吧,可惜两次都由 ...

  9. 北航OO第二单元作业总结(2.1~2.3)

    在经过第一单元初步认识面向对象编程思想后,本蒟蒻开始了第二单元--多线程部分的学习.本单元的作业是构造符合条件的"目的选层电梯"模型,自行设计调度算法,进行合理调度,完成所有乘客的 ...

随机推荐

  1. jpa Auditor 自动赋值与自定义 @CreatedBy @LastModifiedBy @CreatedDate @LastModifiedDate

    在spring jpa audit 中,在字段或者方法上使用注解@CreatedDate.@CreatedBy.@LastModifiedDate.@LastModifiedBy,当进行实体插入或者更 ...

  2. 网络结构---从alexnet 到resnet

    AlexNet-> vgg vgg 采用更小的卷积核,加深网络深度,但两者的共同点都是卷积层+pooling层最后接上fc 层的结构 Network in network ->google ...

  3. JS+CSS实现弹出全屏灰黑色透明遮罩效果的方法

    本文实例讲述了js+CSS实现弹出一个全屏灰黑色透明遮罩效果的方法.分享给大家供大家参考.具体分析如下: 在众多的网站都有这样的效果,当进行一定的操作之后,会弹出一个灰黑色的半透明的遮罩,在上面可以操 ...

  4. go [第一篇]初识

    [第一篇] 简介 Go 是一个开源的编程语言,它能让构造简单.可靠且高效的软件变得容易. Go是从2007年末由Robert Griesemer, Rob Pike, Ken Thompson主持开发 ...

  5. vo类,model类,dto类的作用及划分

    1.entity里的每一个字段,与数据库相对应, 2.dto里的每一个字段,是和你前台页面相对应, 3.VO,这是用来转换从entity到dto,或者从dto到entity的中间的东西.   举个例子 ...

  6. Python3的保留字

    Python3的保留字 false none true and 表示条件的并列,并且条件全部成立 as assert break class continue def del elif else ex ...

  7. MySQL5.7开启独立表空间参数innodb_file_per_table【原创】

    今天在线上某个系统发现MySQL数据库使用的是共享表空间,想修改为独立表空间,操作如下: #因为是主从结构,在从库修改测试,先关闭binlog SET SQL_LOG_BIN=; show varia ...

  8. story 泄露服务器libc版本

    记录下学到的姿势,利用信息泄露得到服务器libc 至少两个函数偏移,利用libc-databse得到服务器libc版本 传送门 泄露脚本如下 from pwn import * context.log ...

  9. 收藏一个可以学习javascript prototype的链接

    https://github.com/mqyqingfeng/Blog/issues/2

  10. 初学python之路-day09

    今天的主要内容为内存管理. 1.引用计数:垃圾回收机制的依据 # 1.变量的值被引用,该值的引用计数 +1 # 2.变量的值被解绑,该值的引用计数 -1 # 3.引用计数为0时就会被垃圾回收机制回收 ...