2022面向对象第二单元总结

第五次作业

UML 类关系图

UML 类协作图

架构分析

  • 生产者-消费者模式
    • 第一级:RequestInput -> RequestQueue -> Dispatcher
      • 用于输入线程和分派器线程之间的交互
    • 第二级: Dispatcher -> RequestTable -> Elevator
      • 用于分派器线程和电梯线程之间的交互
      • Dispathcher 中有五个 RequestTable 对象,对应五个楼座,会把接受到的请求按楼座分派给不同的请求表
      • RequestTable 中有十个 RequestQueue 对象,对应十个楼层,电梯根据所在楼层获得对应的请求队列,从中取出请求
  • \(LOOK\) 策略
    • 如果电梯内有乘客,则直接按原方向运行
    • 接人条件:请求方向和电梯方向相同
    • 转向条件:当前运行方向上没有请求,且反方向(要包括当前楼层,否则会漏接)有请求
    • 线程等待条件:整个楼座均没有请求时,电梯主动 wait,让出资源
  • 量子电梯
    • 对于开关门和移动,我们只需关心等待的时间是否足够,一旦足够,便可以瞬间完成动作
    • 以开关门为例,大致做法如下:
      • 每次开门时记录下当前时间戳 (利用官方包的 println 的返回值),然后 wait(400)
      • 如果当前楼座有新的请求,则 Dispatcher 会唤醒电梯,电梯尝试接人
      • System.currentTimeMillis() 获得当前时间戳,如果时间差 delta >= 400,则瞬间关门
      • 否则再次等待 wait(400 - delta)
    • 优点:在保证总等待时间最短的情况下,尽可能地多接人

同步块的设置和锁的选择

  • RequestQueue 设置为线程安全类,利用 synchronized 同步了 putgetgetAllisEmpty 等方法

  • 利用 static synchronized 包装了线程不安全的官方输出方法

    public static synchronized long println(String s) {
        return TimableOutput.println(s);
    }
    
  • 在电梯休眠和唤醒电梯时,利用 synchronized 获得电梯对象的锁

  • 由于 synchronized 已经能满足需求,所以没有利用 ReentrantLockReentrantReadWriteLock 等显式加锁

调度器设计

  • Dispatcher 作为伪调度器
    • 管理候乘表,将不同请求分派到不同楼座不同楼层
    • 负责管理电梯,并在新请求到来时唤醒相应电梯

第六次作业

UML 类关系图

UML 类协作图

架构分析

  • 沿用了第五次作业的二级生产者 - 消费者模式,纵向 \(LOOK\) 策略,量子电梯
  • 横向电梯 \(LOOK\) 策略
    • 如果电梯内有乘客,则直接按原方向运行
    • 接人条件:请求方向和电梯方向相同,请求方向为使距离更短所走的方向
    • 转向条件:
      • 如果当前位置有乘客未接,则直接转向(否则会导致电梯一直转圈)
      • 如果前方两个楼座没有请求,且后方两个楼座有请求,则转向
    • 线程等待条件:整个楼层均没有请求
  • 调度策略:自由竞争
  • 优化方案:
    • 优先接和电梯内乘客相同目的地的乘客
      • 减少电梯开门次数,从而减少等待时间
      • 经过测试,对于各类型数据性能均有所提升
    • 优先接距离远的请求
      • 先完成时间长的请求,减少电梯总运行时间
      • 经过测试,相比于先来先服务,对于相同出发地的聚集性数据,有较大的性能提升

同步块的设置和锁的选择

  • 相较于上次作业,设置了两个线程安全类 RequestQueuePersonQueue,用 synchronized 同步了 putget 等方法
    • RequestQueue 用于 RequestInputDispatcher 间进行交互,可存储电梯请求和乘客请求
    • PersonQueue 作为 PersonTable 候乘表类的组成部分,在 DispatcherElevator 间进行交互,只存储了乘客请求
  • 仍没有使用 ReentrantLockReentrantReadWriteLock 等锁

调度器设计

  • Dispatcher 作为伪调度器
    • 管理候乘表,将不同请求分派到不同楼座不同楼层,并且分开存储纵向请求和横向请求
    • 负责增加、管理电梯,并在新请求到来时唤醒相应电梯
  • 对于多电梯的调度,在阅读了大量学长的博客,权衡了性能和实现难度之后,最终采用自由竞争策略

第七次作业

UML 类关系图

UML 类协作图

架构分析

  • 沿用了第六次作业的二级生产者 - 消费者模式,纵向、横向 \(LOOK\) 策略,量子电梯以及优化方案
  • 多个生产者对一个消费者
    • Elevator, RequestInput -> PersonQueue -> Dispatcher
    • 当乘客下电梯时,如果此时未到达终点,则重新扔进乘客队列
  • 调度策略:自由竞争
  • 路线规划:
    • 基本策略
      • 由于距离、电梯数量、电梯运行速度等多个因素的影响,不好计算路径的权值,因此并未选择最短路算法
      • 考虑到乘客上下以及等电梯的时间较长,因此遵循尽可能少换乘的策略,保证最多两次换乘
      • 在换乘次数相同的情况下,寻找距离最短的路径,且横向移动优先
      • 综合上面两点,对任意出发地和目的地,最多可能有十种不同的路线,按优先级依次判断这些路线是否可行即可
    • 贪心算法
      • 每次只规划出下一个最优目的地,而不是完整路线
      • 能很好地适应动态规划
    • 实时动态规划
      • 在乘客被电梯接上之前,可随时更新路线
      • 每当新增电梯时,更新候乘表中所有乘客的路线

同步块的设置和锁的选择

  • 设置了两个线程安全类 PersonQueuePersonSet,用 synchronized 同步了 putget 等方法
    • PersonQueue 作为一级托盘,用于 RequestInputElevatorDispatcher 投喂乘客请求
    • PersonSet 作为二级托盘 PersonTable 候乘表类的组成部分,在 DispatcherElevator 间进行交互
  • 利用了 synchronized 可重入的特性,将电梯确定方向以及休眠这一段代码用 synchronized 加锁,这在 bug 分析里会详细介绍
  • 仍没有使用 ReentrantLockReentrantReadWriteLock 等锁

调度器设计

  • Dispatcher 作为伪调度器
    • 管理候乘表,将不同请求分派到不同楼座不同楼层,并且分开存储纵向请求和横向请求
    • 负责增加、管理电梯,并在新请求到来时唤醒相应电梯
    • 规划乘客路线,并在新增电梯时更新路线
  • 对于多电梯的调度仍采用自由竞争策略

数据构造

概述

  • 前两次作业的测试数据基本上为第七次作业的子集,因此只介绍最后一次作业的测试数据
  • 数据总共分为五种模式,每种模式分为五种主题,每个主题又包含四种时间特征,基本上有 \(100\) 种数据类别
  • 请求数量、输入时间、电梯总数量、每条路线上电梯数量均可控,改一下参数即可满足强测和互测的不同要求
  • 除此之外还有一些特殊数据

五种模式

模式 乘客请求特征 电梯请求特征 功能
全面模式 任意起点终点 任意增加电梯 测试整个系统的正确性
单楼座 所有请求的起点和终点在同一楼座 只在这一楼座增加纵向电梯 测试纵向单电梯运行和多电梯调度
单楼层 所有请求的起点和终点在同一楼层 只在这一楼层增加横向电梯 测试横向单电梯运行和多电梯调度
双楼座 所有请求的起点和终点在两个楼座 只在这两个楼座增加纵向电梯,横向电梯任意 测试换乘
双楼层 所有请求的起点和终点在两个楼层 只在这两个楼层增加横向电梯,纵向电梯任意 测试换乘

五种主题

主题 特征 功能
随机 任意起点终点 测试整个系统的正确性
单请求 多个电梯请求 + 单个乘客请求 测试系统的启动和停止
单方向 同一楼座或楼层的请求方向相同 测试运行策略和调度策略
最远距离 纵向请求起点终点必须为 1 或 10,横向请求起点终点必须为 A 或 E 测试运行策略和调度策略
同出发地 每次会连续投喂 8 - 10 个相同出发地的请求 测试满载、运行策略和调度策略

四种时间特征

  • 零时瞬时输入:所有请求均在 0 秒时同时输入
  • 非零瞬时输入:所有请求在不是 0 秒时同时输入
  • 聚集输入:所有请求在 1 秒的时间间隔内完成输入
  • 分散输入:所有请求在 \(n\) 秒的时间间隔内完成输入,其中 \(n\) 为请求总数

特殊数据

  • 使五部电梯行为完全一致的数据

    • 用于 hack 第一次作业的输出线程安全问题
    • 该数据一次性把房里 5 个输出线程不安全的人都 hack 出来了
    [1.0]1-FROM-A-10-TO-A-1
    [1.0]2-FROM-B-10-TO-B-1
    [1.0]3-FROM-C-10-TO-C-1
    [1.0]4-FROM-D-10-TO-D-1
    [1.0]5-FROM-E-10-TO-E-1
    ...
    
  • 压力测试数据

    • 50 电梯 5000 请求、批量聚集输入
    • 用于测试大规模请求下电梯系统的正确性

自动化测试

基本流程

  • 编译打包

    os.system("javac -encoding UTF-8 -cp " + JAR_PATH + " -d class/ -sourcepath src/ " + main_path)
    
    with open("MANIFEST.MF", "w") as mf:
        mf.write("Manifest-Version: 1.0\nMain-Class: " + self.main_class + "\n")
    os.system("jar -cfm code.jar MANIFEST.MF -C class/ .")
    os.system("jar -uf code.jar -C " + OFFICIAL_PATH + " .")
    
  • 运行

    in_pro = Popen("datainput_student_win64.exe", shell=True, cwd=WORK_PATH, stdout=PIPE)
    cmd = "java -jar " + self.player_path + "code.jar"
    with open(self.player_path + "res/out{0}.txt".format(i), "w") as out:
    	self.processes.append(Popen(
    		cmd, cwd=WORK_PATH, encoding="UTF-8", stdin=in_pro.stdout, stdout=out, stderr=out))
    
  • 检查正确性:具体见下面的 SPJ 设计

SPJ 设计

  • 利用异常抛出和捕获机制

    • 如果在某一环节检查出了问题,直接抛出相应异常,由最外层的 check 函数捕获,输出相应错误信息,并保存信息到本地
    • 因为要检查的地方太多(大概有20多种),如果用分支结构会使得程序结构特别复杂
  • 超时检查

    • 设置运行时间上限为 250s process.communicate(timeout=250),并捕获 TimeoutExpired 异常。如果超时,则 kill 当前进程。错误类型为 RunTimeExceed
    • 参考了学长的代码,利用 ctypes 库获得程序运行时间和 cpu 时间。错误类型分别为 RealTimeExceedCPUTimeExceed
  • 输出格式检查

    • 利用正则匹配每一行的输出信息,如果匹配失败,则格式错误,一般情况下为 java 抛了异常。错误类型为 WrongDataFormat(如果把标准输出 out 和 标准错误err 分开定向,可以精确识别异常,但没有这么做的目的是为了更好地还原运行时的状态)
    • 电梯 id 是否存在。错误类型为 ElevatorNotExist
    • 输出时间序列是否递增。错误类型为 TimeReverse
  • 电梯行为检查

    • 对于五种行为,检查是否合理的同时改变电梯状态。每种行为可能出现的错误如下:

    • \(ARRIVE\)

      错误 错误类型
      走之前未关门 MoveBeforeClose
      等待时间不足 MoveTimeNotEnough
      楼座、楼层等越界 FloorOutOfBoundsBuildingOutOfBounds
      重复到达 AlreadyArrive
      瞬移两层及以上 FloorJumpBuildingJump
      电梯错位(比如A座电梯跑到B座) FloorDiffBuildingDiff
    • \(OPEN\)

      错误 错误类型
      已经打开 AlreadyOpen
      横向电梯不可开门 Can'tOpen
    • \(CLOSE\)

      错误 错误类型
      重复关门 AlreadyClose
      开关门时间不足 OpenCloseTimeNotEnough
    • \(IN\)

      错误 错误类型
      电梯未开门 NotOpenButIn
      乘客不存在 PersonNotExist
      乘客不在这(位置不对、已上电梯、未下电梯等) PersonNotHere
      超载 OverLoad
    • \(OUT\)

      错误 错误类型
      电梯未开门 NotOpenButOut
      乘客不存在 PersonNotExist
      乘客不在这(不在这个电梯、已下电梯等) PersonNotHere
  • 结束状态检查

    • 电梯是否关门。错误类型为 ElevatorNotClose
    • 乘客是否到站。错误类型为 PersonNotArrive
  • 记录信息

    • 正确:输出运行时间、cpu 时间、总等待时间等,用于比较性能
    • 错误:记录错误类型,错误发生时的现场状态(乘客、电梯等),错误输出,以及按电梯 id 分类后的输出

多进程并行测试

  • 利用 Popen,可开多个进程并行测试
    • 用一列表管理所有 Popen 对象,之后依次 communicate 即可
    • 注意要把每个进程的输出重定向到不同的位置,而不能全用 PIPE,否则会因为 PIPE 空间不够,导致未占用 PIPE 的进程阻塞,使得输出的时间戳跳变
    • 如果在 communicate 中设置了 timeout,要记得 kill 掉进程,否则当电梯系统无法停止时,它也会一直运行下去(即使python程序结束了),导致无法进行下一次启动
  • 可实现房里 7 个人的多程序联测,对于电梯这种运行时间长的程序,可大大提升测试效率
  • 可对一个人的程序多进程跑点,能很好的 hack 到复现率较低的 bug

bug 分析

本人 bug

  • OJ
    • 第六次作业互测被刀了一个点
      • 错误原因:当纵向电梯处于 1 层时,如果在电梯尝试接完人(且没有接到)和判断运行方向这一间隔内,1 层来了请求,则电梯会转向,走到 0 层。程序逻辑如下:电梯没有接到人 -> 1 层来了人 -> 电梯发现 1 层有没接的人 -> 电梯转向
      • 解决方案:在判断方向时增加特殊判断,保证电梯在 1 层时只能向上,在 10 层时只能向下
    • 其他强测和互测没有被测出 bug
  • 本地
    • 第七次作业发现一个典型的 bug,而且和互测被刀的点逻辑很像
      • 错误原因:候乘表没有请求,电梯判断出应该 wait -> 来了请求 -> 分派器 notify 电梯 -> 电梯 wait,此时分派器因为还存在没有完成的请求而不会停止,而电梯也会陷入无限的 wait
      • 解决方案:将电梯确定方向以及休眠这一段代码用 synchronized 加锁,防止在 wait 之前就被 notify

他人 bug

  • 第五次作业:hack 了 5 个人 6 个 bug

    • 5 个人输出线程不安全:TimeReverse
    • 其中一个人因为策略原因运行时间超时:RealTimeExceed
  • 第六次作业:hack 了 4 个人 5 个 bug

    saber: NullPointerException, CPUTimeExceed
    lancer: AlreadyArrive
    berserker: Overload
    alterego: CPUTimeExceed
    
  • 第七次作业:hack 了 6 个人 10 个 bug,但因为复现率低以及挡刀的问题,oj 只测出来 5 个 bug

    saber: RunTimeExceed
    lancer: RunTimeExceed
    rider: PersonNotHere
    caster: ConcurrentModificationException, Can'tOpen, RunTimeExceed
    assassin: PersonNotHere, PersonNotArrive, RunTimeExceed
    alterego: PersonNotArrive
    

测试策略有效性

  • 三次作业均有 hack,且数量较多
  • 不存在别人 hack 到的 bug 我没 hack 到,只存在我 hack 到的别人没 hack 到
  • 测出了很多复现率极低的 bug (第六次作业被刀后,引入了多进程跑点,使得测试强度有了质的提高)
  • 综上来看,所构造的数据以及测评机是有较高强度的,应该能超过强测和互测

心得体会

线程安全设计

  • 学会了 synchronized 同步块的使用
  • 学会了原子类 AtomicIntegerAtomicBoolean 等的使用
  • 对于 ReentrantLockReentrantReadWriteLock 等锁有了一定了解
  • 对于 BlockingQueue 等线程安全的容器有了一定了解

层次化设计

  • 相较于第一单元作业层次化并不明显
  • 整体框架使用了两级托盘,使得不同的生产者和消费者具有不同的层次

数据构造与自动化测试

  • 电梯月的精力全放在了 OO 上,而 OO 上的精力主要放在了造数据和写测评机。整个代码量达到一千多行,甚至比电梯还多的多(
  • 相比于以往测评机的简单对拍,这次实现了较为复杂的 special judge
  • 实现了多进程并发测试,对于运行时间较长的程序能极大的提升效率,且便于测试难以复现的点
  • 数据构造和写测评机的能力有了很大提升

不足之处

  • 没有实现真正意义上的调度器,而是选择自由竞争策略。但是代码写起来确实简单,三次强测也都 99 + ,还是很香的
  • 第六次作业本地测试力度不够,没有进行多进程跑点,导致被 hack 了一个复现率极低的 bug,与金刚无缘了
  • 三次作业只有整体框架一致,但细节上都是只考虑到当前需求,可扩展性不强,因此每一次都进行了小规模重构
posted @ 2022-04-30 17:47  t0ush1  阅读(61)  评论(1编辑  收藏  举报