BUAA_OO_2022_Unit_2_Summary

一、程序设计构架

第一次作业

  • 需求简述:

    模拟一个多线程实时电梯系统,各楼座有且仅有一台纵向电梯,处理已知起终点的同楼座乘客请求。

  • 代码构架:

    |- Unit2:主类
    |- InputHandler:输入线程
    |- Controller:各楼座候乘表
    |- Elevator:电梯线程
    |- OutputHandler:输出处理
    

    采用生产者-消费者设计模式,托盘为各楼座候乘表,即实际上有5张候乘表。

    同一楼座的电梯共享该楼座的候乘表,读写信息均在候乘表中加锁实现,避免几个线程之间直接交互以及数据不同步的问题。

    对 TimableOutput 加锁封装为输出处理类,避免出现以下情况导致输出时间戳非递增:

    1获取时间戳 --> 2获取时间戳 --> 2输出 --> 1输出
    
  • 规模分析:

    共221个代码行

  • 复杂度分析:

    method ev(G) iv(G) v(G)
    Controller.addRequest(PersonRequest) 1.0 1.0 1.0
    Controller.getRequests() 1.0 1.0 1.0
    Controller.isInputAlive() 1.0 1.0 1.0
    Controller.setInputAlive(boolean) 1.0 1.0 1.0
    Controller.setRequests(ArrayList) 1.0 1.0 1.0
    Elevator.Elevator(char, int, Controller) 1.0 1.0 1.0
    Elevator.move(long) 1.0 1.0 2.0
    Elevator.openAndClose() 1.0 13.0 15.0
    InputHandler.InputHandler(ElevatorInput, HashMap) 1.0 1.0 1.0
    OutputHandler.initStartTimestamp() 1.0 1.0 1.0
    OutputHandler.println(String) 1.0 1.0 1.0
    Unit2.main(String[]) 1.0 3.0 3.0
    InputHandler.run() 3.0 6.0 6.0
    Elevator.run() 9.0 22.0 25.0
    Total 24.0 54.0 60.0
    Average 1.71 3.86 4.29

第二次作业

  • 需求简述:

    模拟一个多线程实时电梯系统,初始各楼座有且仅有一台纵向电梯;可动态增加横向、纵向电梯;处理已知起终点的同楼座或同楼层乘客请求。

  • 代码构架:

    |- Unit2:主类
    |- InputHandler:输入线程
    |- GlobalController:总候乘表
    |- Controller:各楼座/层候乘表
    |- Elevator:电梯线程
    	|- ElevatorP:横向电梯线程
    	|- ElevatorV:纵向电梯线程
    |- OutputHandler:输出处理
    

    仅为完成本次作业的代码与第一次作业基本相同,只是将电梯线程抽象出来——把方向计算、距离计算抽象成子函数,并在横向、纵向电梯类内具体实现这些操作。此时,两类电梯的开关门、运行、捎带等策略完全一致,可以将第一次作业的代码完全复用,并且没有出现大段重复代码。

    因上述实现较为简单,我在本次作业中尝试提前实现换乘。将原来的一级托盘结构更改为二级托盘结构,分别装载所有请求和具体座/层请求。详见后文“调度器设计”。

  • 规模分析:

    共489个代码行

  • 复杂度分析:

    method ev(G) iv(G) v(G)
    Controller.addRequest(PersonRequest) 1.0 1.0 1.0
    Controller.getRequests() 1.0 1.0 1.0
    Controller.isInputAlive() 1.0 1.0 1.0
    Controller.setInputAlive(boolean) 1.0 1.0 1.0
    Controller.setRequests(ArrayList) 1.0 1.0 1.0
    Elevator.Elevator(int, GlobalController, Controller) 1.0 1.0 1.0
    Elevator.getBlock() 1.0 1.0 1.0
    Elevator.getDirection() 1.0 1.0 1.0
    Elevator.getEleId() 1.0 1.0 1.0
    Elevator.getFloor() 1.0 1.0 1.0
    Elevator.inDir(PersonRequest) 1.0 1.0 1.0
    Elevator.openAndClose() 1.0 5.0 5.0
    Elevator.setBlock(char) 1.0 1.0 1.0
    Elevator.setCtrlKey(String) 1.0 1.0 1.0
    Elevator.setDirection(int) 1.0 1.0 1.0
    Elevator.setFloor(int) 1.0 1.0 1.0
    Elevator.takeIn(String) 1.0 11.0 11.0
    Elevator.takeOff(String) 1.0 4.0 4.0
    Elevator.waitInDir(PersonRequest) 1.0 1.0 1.0
    ElevatorP.ElevatorP(int, int, GlobalController, Controller) 1.0 1.0 1.0
    ElevatorP.isFrom(PersonRequest) 1.0 1.0 1.0
    ElevatorP.isTo(PersonRequest) 1.0 1.0 1.0
    ElevatorP.move(long) 1.0 4.0 4.0
    ElevatorP.reqDir(PersonRequest) 1.0 1.0 3.0
    ElevatorP.waitDir(PersonRequest) 1.0 1.0 3.0
    ElevatorV.ElevatorV(char, int, GlobalController, Controller) 1.0 1.0 1.0
    ElevatorV.isFrom(PersonRequest) 1.0 1.0 1.0
    ElevatorV.isTo(PersonRequest) 1.0 1.0 1.0
    ElevatorV.reqDir(PersonRequest) 1.0 1.0 1.0
    ElevatorV.waitDir(PersonRequest) 1.0 1.0 1.0
    GlobalController.GlobalController() 1.0 1.0 1.0
    GlobalController.addController(ElevatorRequest) 1.0 2.0 2.0
    GlobalController.addElevator(ElevatorRequest) 1.0 3.0 3.0
    GlobalController.addElevatorP(ElevatorRequest) 1.0 2.0 2.0
    GlobalController.addElevatorV(ElevatorRequest) 1.0 2.0 2.0
    GlobalController.infoInputKilled() 1.0 2.0 2.0
    GlobalController.initControllers() 1.0 2.0 2.0
    GlobalController.initElevators() 1.0 2.0 2.0
    GlobalController.withoutTransfers() 1.0 1.0 1.0
    InputHandler.InputHandler(ElevatorInput) 1.0 1.0 1.0
    OutputHandler.initStartTimestamp() 1.0 1.0 1.0
    OutputHandler.println(String) 1.0 1.0 1.0
    Unit2.main(String[]) 1.0 1.0 1.0
    GlobalController.addNextRequest(String, PersonRequest) 3.0 4.0 5.0
    InputHandler.run() 3.0 6.0 6.0
    ElevatorV.move(long) 4.0 4.0 4.0
    Elevator.ifOpen() 5.0 11.0 12.0
    Elevator.run() 5.0 12.0 14.0
    GlobalController.addRequest(PersonRequest) 7.0 8.0 10.0
    Total 70.0 116.0 126.0
    Average 1.43 2.37 2.57

第三次作业

  • 需求简述:

    模拟一个多线程实时电梯系统,初始各楼座有且仅有一台纵向电梯、1层有且仅有一台可达所有楼座的横向电梯;可动态增加横向、纵向电梯,新电梯可自定义限载人数、运行速度、(横向电梯)可达楼座;处理已知起终点的任意乘客请求。

  • 代码构架

    |- Unit2:主类
    |- InputHandler:输入线程
    |- GlobalController:总候乘表
    |- Controller:各楼座/层候乘表
    |- Elevator:电梯线程
    	|- ElevatorP:横向电梯线程
    	|- ElevatorV:纵向电梯线程
    |- OutputHandler:输出处理
    
  • 规模分析:

    共557个代码行

  • 复杂度分析:

    method ev(G) iv(G) v(G)
    Controller.addElevator(ElevatorRequest) 1.0 1.0 1.0
    Controller.addRequest(PersonRequest) 1.0 1.0 1.0
    Controller.getElevators() 1.0 1.0 1.0
    Controller.getRequests() 1.0 1.0 1.0
    Controller.isInputAlive() 1.0 1.0 1.0
    Controller.setInputAlive(boolean) 1.0 1.0 1.0
    Controller.setRequests(ArrayList) 1.0 1.0 1.0
    Elevator.Elevator(int, int, long, GlobalController, Controller) 1.0 1.0 1.0
    Elevator.getBlock() 1.0 1.0 1.0
    Elevator.getCtrl() 1.0 1.0 1.0
    Elevator.getDirection() 1.0 1.0 1.0
    Elevator.getEleId() 1.0 1.0 1.0
    Elevator.getFloor() 1.0 1.0 1.0
    Elevator.inDir(PersonRequest) 1.0 1.0 1.0
    Elevator.move(long) 1.0 1.0 1.0
    Elevator.openAndClose() 1.0 5.0 5.0
    Elevator.setBlock(char) 1.0 1.0 1.0
    Elevator.setCtrlKey(String) 1.0 1.0 1.0
    Elevator.setDirection(int) 1.0 1.0 1.0
    Elevator.setFloor(int) 1.0 1.0 1.0
    Elevator.specialWait() 1.0 1.0 1.0
    Elevator.takeIn(String) 1.0 13.0 13.0
    Elevator.takeOff(String) 1.0 4.0 4.0
    Elevator.waitInDir(PersonRequest) 1.0 1.0 1.0
    ElevatorP.ElevatorP(int, int, int, long, int, GlobalController, Controller) 1.0 1.0 1.0
    ElevatorP.canCarry(PersonRequest) 1.0 1.0 1.0
    ElevatorP.canStop() 1.0 1.0 1.0
    ElevatorP.isFrom(PersonRequest) 1.0 1.0 1.0
    ElevatorP.isTo(PersonRequest) 1.0 1.0 1.0
    ElevatorP.move(long) 1.0 4.0 4.0
    ElevatorP.reqDir(PersonRequest) 1.0 1.0 3.0
    ElevatorP.waitDir(PersonRequest) 1.0 1.0 3.0
    ElevatorV.ElevatorV(char, int, int, long, GlobalController, Controller) 1.0 1.0 1.0
    ElevatorV.canCarry(PersonRequest) 1.0 1.0 1.0
    ElevatorV.canStop() 1.0 1.0 1.0
    ElevatorV.isFrom(PersonRequest) 1.0 1.0 1.0
    ElevatorV.isTo(PersonRequest) 1.0 1.0 1.0
    ElevatorV.reqDir(PersonRequest) 1.0 1.0 1.0
    ElevatorV.waitDir(PersonRequest) 1.0 1.0 1.0
    GlobalController.GlobalController() 1.0 1.0 1.0
    GlobalController.addController(ElevatorRequest) 1.0 2.0 2.0
    GlobalController.addElevator(ElevatorRequest) 1.0 3.0 3.0
    GlobalController.addElevatorP(ElevatorRequest) 1.0 2.0 2.0
    GlobalController.addElevatorV(ElevatorRequest) 1.0 2.0 2.0
    GlobalController.addRequest(PersonRequest) 1.0 5.0 5.0
    GlobalController.infoInputKilled() 1.0 2.0 2.0
    GlobalController.initControllers() 1.0 2.0 2.0
    GlobalController.initElevators() 1.0 2.0 2.0
    GlobalController.withoutTransfers() 1.0 1.0 1.0
    InputHandler.InputHandler(ElevatorInput) 1.0 1.0 1.0
    OutputHandler.initStartTimestamp() 1.0 1.0 1.0
    OutputHandler.println(String) 1.0 1.0 1.0
    Unit2.main(String[]) 1.0 1.0 1.0
    ElevatorP.specialWait() 3.0 2.0 3.0
    GlobalController.addNextRequest(String, PersonRequest) 3.0 4.0 5.0
    InputHandler.run() 3.0 6.0 6.0
    ElevatorV.move(long) 4.0 4.0 4.0
    Elevator.run() 5.0 13.0 15.0
    GlobalController.findTranFloor(char, char, int, int) 5.0 3.0 6.0
    Elevator.ifOpen() 6.0 12.0 14.0
    Total 82.0 131.0 144.0
    Average 1.37 2.18 2.40

可扩展性分析

我认为我的三次作业都有很强的可扩展性,主要体现在电梯类:

  • 电梯属性(编号、层/座、载客量、速度等)都设为类内属性,而非直接用具体数字代替。可以快速实现电梯自定义的要求。
  • 电梯行为(开关门、上下客、移动)都写为行为函数,逻辑清晰、易于定位。
  • 一些状态的判断(是否有乘客在某方向等待、乘客目标方向是否符合捎带要求等)也写为独立抽象函数,可以在具体类型的电梯子类中重写具体实现。可以实现更多种类的电梯,倾斜、跃层运行的新型电梯等。

协作图

附:度量分析条目解释

  • OC:类的非抽象方法圈复杂度,继承类不计入
  • WMC:类的总圈复杂度
  • ev(G):非抽象方法的基本复杂度,用以衡量一个方法的控制流结构缺陷,范围是 [1, v(G)]
  • iv(G):方法的设计复杂度,用以衡量方法控制流与其他方法之间的耦合程度,范围是 [1, v(G)]
  • v(G):非抽象方法的圈复杂度,用以衡量每个方法中不同执行路径的数量

二、调度器设计

第一次作业

写代码的时候我一直希望写一个没有调度器(依赖JVM调度)的电梯系统。我以为的调度器是将请求一个一个喂给电梯,具体到电梯什么时候去哪做什么。后来读了上机实验的代码之后才发现,简单地将请求分派到5张候乘表里就是调度器功能的一部分。

因此,第一次作业的调度器被我糅合进了 InputHandler 里面,在读入请求时就将请求分派到了对应的候乘表内。Controller 实际只承担了作为同步容器的职能,并不作为实际的调度器而存在。

InputHandler:读入请求,将请求添加到对应楼座的候乘表内。(实际调度器)
Controller:保存当前楼座所有未处理的请求,供给电梯查阅。

第二次作业

本次作业我提前实现了跨楼层/座的换乘。换乘涉及到请求的回写,不能直接沿用上次作业的候乘表设计,否则可能出现以下情况:

*请求:A座1层 -- B座4层(存在1层横向电梯)

时间 1层候乘表请求 B座候乘表请求 动作 对应处理
1 A1 -- B1 -- B4 读入线程结束 B座所有电梯线程结束
2 1层电梯接到乘客 --> 送到B1 子请求回写至B座候乘表
3 B1 -- B4

可见,回写请求时,B座电梯线程已结束,无法再将乘客送到B座4层,换乘失败。

所以,我需要一个总的候乘表 GlobalController 装载整个电梯系统的请求。只有当所有请求(包括待换乘的请求)全部被处理完毕后,才能通知电梯结束线程。

GlobalController 获取新请求时,首先将其拆分为1~3个子请求,利用 HashMap 通过 Key 可获得 Value 的特性 以及 乘客ID的各异性 ,将请求链装入 HashMap 中。

GlobalController:获取请求,拆分,将第一个子请求投入对应候乘表;
				子请求结束,查询后继子请求,再投入对应候乘表。
Controller:保存当前楼座所有未处理的请求,供给电梯查阅。

第三次作业

本次作业沿用上次拥有的换乘功能的调度器,区别仅在于拆分请求时需额外考虑中转楼层横向电梯的可达性。

三、同步块与锁分析

本单元代码中的所有用到共享对象的部分,我都选择了使用 synchronized 加锁。主要用途如下:

  • 共享对象的类方法

    如果方法涉及到读写本类内属性的操作,给该方法上锁。

// Controller.java
// requests为类内属性
public synchronized void addRequest(PersonRequest pr) {
	requests.add(pr);
	this.notifyAll();
}
  • 线程对象中的同步块

    如果线程对象中涉及到连续读写共享对象,且上下文连接紧密,不可中断的操作,给该代码块上锁。

    设置同步块的大多数场景为:电梯对象遍历访问候乘表的所有请求。此时须保证遍历的全过程中候乘表不能被更改,否则将会出现线程不安全错误。

// ElevatorP.java
// getCtrl()返回共享对象
synchronized (getCtrl()) {
	for (PersonRequest x: getCtrl().getRequests()) {
		char from = x.getFromBuilding();
		char to = x.getToBuilding();
		if (((switchInfo >> (from - 'A')) & 1) +
			((switchInfo >> (to - 'A')) & 1) == 2) {
			return false;
		}
	}
}

在第一次作业中,我的共享对象全部是同一个类的实例,且共享对象之间不会有互相访问的情况。因此,用最粗暴的方法“只要涉及共享对象,全部上锁!”不会导致死锁。

从第二次作业开始,我将原来的生产-消费模式中的一级托盘重构为二级托盘。因此在实现时特别留意,防止两类共享对象肆意加锁导致的死锁。具体策略如下:

  • 可以锁住一级托盘访问二级托盘,二级托盘锁内不可访问一级托盘
// GlobalController.java
// 一级托盘:GlobalController,二级托盘:Controller
public synchronized void infoInputKilled() {
	this.inputAlive = false;
	for (Controller ctrl : controllers.values()) {
		synchronized (ctrl) {
 			ctrl.setInputAlive(false);
 			ctrl.notifyAll();
		}
 	}
}
  • 线程对象中,二级托盘同步块内避免调用其他共享对象的(加锁)方法、避免二次设置同步块
// Elevator.java
// gctrl为一级托盘对象,ctrl为二级托盘对象
boolean transEnd = gctrl.withoutTransfers();	// 在二级托盘同步块外调用一级托盘
synchronized (ctrl) {
    if ((ctrl.getRequests().isEmpty() || specialWait()) && direction == 0) {
	if (ctrl.isInputAlive() || !transEnd) {		// *
			...
        } else { return; }
    } else {
        ...
    }
}

由于我的同步块内基本上都是读写并举,改用读写锁的轻量化意义不大,所以全程只用了 synchronized 的方式上锁。

四、电梯逻辑分析

运行策略:自由竞争,目标明确,LOOK捎带

自由竞争

  • 候乘表内请求对所有电梯可见

    可以说,电梯之间互相不知道对方的存在,都以为该楼座/层只有自己一台电梯。因此,在有条件接人/捎带的时候,它们都会尽自己最大的努力去满足候乘表内的乘客请求。当乘客进入电梯时,该请求才会被移出候乘表。

    这个思想有点像操作系统中的进程——“每个进程都以为自己独占整个虚存空间,并且不遗余力地占满。”(来源自pyq文案,侵删)

  • 同时出发竞争请求

    当请求到来时,所有空闲电梯一起出发接人;所有非空闲电梯按原路线前进,照例进行捎带。直到乘客成功进入某电梯,这场竞争才落下帷幕,“该送送,该停停”。

目标明确

  • 坚持到达第一个进入缓冲区的请求起点

    每台电梯设有一个请求缓冲区(waits)。当电梯空闲时,坚持朝向第一个请求的起点方向移动。

  • 坚持将电梯内乘客送到终点

    每台电梯设有一个已搭载乘客列表(ins)。一旦电梯内有乘客,则电梯一定朝向乘客的目的地方向移动。

对电梯移动方向的影响优先级:ins > waits

LOOK捎带

  • 同向捎带,应接尽接

    经过每个楼层时,只要该楼层有乘客等待,且目的地方向与此刻运行方向相同,就捎带该乘客。不考虑新乘客的目的地是否比当前目的地更远或更近。

运行逻辑如下表所示:

性能优化

  • 运行时间间隔

    每次 ARRIVE 或 CLOSE 后记录当前时刻 time 。下一次 ARRIVE 之前的 sleep 时长仅为 max(moveTime - currentTime + time, 0)

    • 节省每层初始判断终止线程、判断是否开门时的遍历、等待锁所消耗的时间。

    • 在电梯由阻塞态被唤醒至运行态时,可“瞬移”至下一状态,看起来像是预判到了新请求。例:

      [5.0000]电梯(ARRIVE-OPEN-OUT-)CLOSE于5层,记录时刻 time = 5.0000,即:
      	[5.0000]CLOSE-A-5-1
      [5.0010]电梯因无乘客、无请求而进入阻塞态
      [6.0000]新请求(1层-3层)到来,电梯被唤醒
      [6.0010]电梯sleep时长为 max(moveTime - 6.0010 + 5.0000, 0)) = 0,直接到达4层,即:
      	[6.0010]ARRIVE-A-4-1
      
  • 开关门时间间隔

    与上述运行时间间隔方法一致,OPEN 后记录当前时刻,缩短 CLOSE 前的 sleep 时长。

    • 节省开门后判断是否让乘客出入、设定电梯运行方向、等待锁所消耗的时间。
  • 开门窗口进出安排

    • 有序搭乘电梯,先下后上。
    • 刚开门就送到达目的地的乘客离开,便于尽快换乘、减少乘客等待时间。
    • 先 sleep 一段时间再开始让乘客进入电梯,尽可能多地接到要在这个地点进入电梯的新乘客。

五、程序BUG分析

横向不可达电梯轮询电梯换向问题

在我的设计中,电梯没有直接的换向逻辑。想要换向,必须经过 上行-暂停-下行 的变化过程。因此,电梯暂停的逻辑十分重要。

前两次作业强测都没有bug,但互测都各被发现了一个bug,原因都是同一个:电梯内无人且缓冲区请求与电梯运行方向相反时,电梯没有停下来而后换向

第一次作业被刀中的bug是我自己在强测之前就de完的,但是最后一版代码没有提交。(过于愤怒,不予分析 TAT)但第二次作业被刀中的问题其实第一次作业debug不严谨导致的。修改了一个部分的暂停逻辑后,没有检查相应的关联逻辑。(真讨厌,为啥不在一次互测里全给我找出来呢!!)

第一次作业de的暂停bug是 (电梯关门后 &)电梯移动一层/栋前 的状态更新。这个阶段的状态更新变化,导致此前 有乘客离开电梯后 & 有乘客进入电梯前 的状态更新逻辑不完整被暴露出来了。原始的程序中,有乘客离开电梯后 & 有乘客进入电梯前 的状态更新可以不严谨,因为 (电梯关门后 &)电梯移动一层/栋前 是它的后续代码块可作为逻辑兜底。一次debug后,这份保障没了,终于在第二次互测被刀出来了。

但是!其实这两个bug想要不被刀到有个很简单的方法:给电梯加个限制——到1层还想往下走,或到10层还想往上走,就强制停止。尽管这个方法用完之后程序里依然有逻辑错误,但能保证不出错,尽管性能会差一点。(可惜我非常相信我电梯的逻辑,有被建议过加限制,可我就是不听……因为我觉得这会影响我找bug……)

是个教训吧——不管程序有多么严谨,边界的保护也还是需要有的。这个边界保护可能用不上(最好),但是如果真的有bug,边界保护作为最后一道防线,起码能保证程序不崩。

// 1 origin
if (open) {
    try { arriveTime = openAndClose(); }
    catch (InterruptedException e) { e.printStackTrace(); }
}
if (ins.isEmpty() && waits.isEmpty() && direction == 0) {
	continue;
}
try { arriveTime = move(arriveTime); }
catch (InterruptedException e) { e.printStackTrace(); }

// 1 debug
if (open) {
    try { arriveTime = openAndClose(); }
    catch (InterruptedException e) { e.printStackTrace(); }
}
if (direction == 0) {
    continue;
}
try { arriveTime = move(arriveTime); }
catch (InterruptedException e) { e.printStackTrace(); }
// 2 origin
takeOff(msg);
if (ins.isEmpty()) {
	direction = 0;
}
takeIn(msg);

// 2 debug
takeOff(msg);
if (ins.isEmpty()) {
	direction = 0;
    if (!waits.isEmpty()) {
        int minDtc = 10;
        for (PersonRequest x: waits) {
            if (abs(waitDir(x)) < minDtc) {
                direction = waitDir(x);
                minDtc = abs(waitDir(x));
            }
        }
    }
}
takeIn(msg);

横向不可达电梯轮询

原有的进入阻塞态逻辑(适用于一二次作业)是 候乘表空 && 轿厢空。然而,由于第三次作业新增了横向电梯可达性要求,电梯系统运行时可能存在这样的情况:该层候乘表非空,但表内所有请求都只能由A电梯运送。此时,该层的另一台电梯B已经没有运送任务了,但始终无法进入阻塞态,只能等待A电梯任务结束才能一起结束线程。因此,在A电梯任务期间,B电梯轮询,导致CTLE。

修复主体在于ElevatorP.java,增加了一个函数specialWait()来改变横向电梯的wait条件:“该楼层请求为空即wait” → “该楼层请求都不是我能处理的即wait”。

其他所有在Elevator.java中的修改都是为了这个函数的实现:
①增加getCtrl()方法,让电梯类能访问对应楼层/座的候乘表;
②增加specialWait()方法,默认返回false(适用于纵向电梯),横向电梯类重写该方法;
③在wait的判断条件中增加specialWait()

六、自测/互测策略

数据构造

本单元的自测互测阶段,我并没有找到很好的构造数据的方法,主要还是随机数据,试图以量取胜。

唯一的一点增加数据强度的策略就是

  • 时间密集:所有请求的投入间隔时间极短,或全部同时投入

    • 引发输出线程不安全问题
    • 引发共享对象没锁好的问题
  • 空间密集:所有请求聚集在单一楼座和楼层(第二次作业);请求起点聚集在同一点(第三次作业)

    • 引发多台电梯同时抢夺时的同步问题

虽然这些数据效果很一般(居然没找出我所有的bug!),但我还是在互测环节有一点点收获,起码能把被刀扣掉的1分捞回来(乐

正确性判断

判断程序输出是否出现以下问题:

# 1输出时间非递增?
# 2进出电梯窗口错误?
# 3.1进电梯错误(重复进)?
# 3.2进电梯错误(错起点)?
# 4.1出电梯错误(重复出)?
# 4.2出电梯错误(非允许楼座)?
# 5.1跃层运行?
# 5.2跃栋运行?
# 5.3倾斜运行?
# 6.1运行时间不足?
# 6.2开门时间不足?
# 7未到即开门?
# 8未开门即关门?
# 9有人没进/没到?
# 10没关门?
# 11超载?
# 12运行超出范围?

对比第一单元的策略差异

  • 两个单元的测试最大的差异在于,第一单元存在标答,第二单元不存在

这个“标答”指的不是输出的标答,而是它拥有一个能转化为统一答案的方法,比如第一单元可以代数值运算,每一个数值都会对应一个标准的表达式值答案。

第二单元没有标答之后,所有的测试标准都需要自己考虑,比如上文提到的1~12点正确性判断标准。这时候,测评机的效果就因人而异了,很有可能因为没想到这个问题而漏掉了某条评判标准。同时,这种“没想到”,会对应到自己的程序设计中,导致有bug而不自知,反而自信满满地提交(别骂了。

  • 多线程的调度是不确定的,除了要求数据强之外,还要求测试量足够大

有些不明显的问题,即使很强的数据也有可能要重复跑10+次才能复现。而且程序本身运行的时间跨度也长。所有,这个单元的测试真的很需要耐心。

七、心得体会

线程安全

要想线程安全,就得保证共享资源的安全。

  • 划清共享资源的边界

这一单元采用的生产者-消费者模式就很好地划定了这个边界——只有托盘是共享资源,生产者和消费者之间产生的所有联系都依赖托盘进行,无互相访问的权限。

  • 梳理清楚访问资源的需求和顺序

刚开始采用一级托盘结构时,完全不需要考虑申请多个资源时产生的死锁问题。到了二级托盘,或说线程对象可以访问到的共享资源不止一个的时候,就需要在设计阶段就梳理清楚线程对象对几个资源的访问需求和顺序,杜绝锁A内申请资源B,而锁B中又申请锁A,导致死锁的情况。

层次化设计

设计模式是对层次化设计的一个很好的提示,或说是限制。生产者、托盘、消费者各司其职,高内聚低耦合地完成总要求的各个子任务:InputHandler(生产者)只管处理输入,Controller、GlobalController(托盘)只管管理(或者说“存着”)乘客请求,Elevator(ElevatorP、ElevatorV)(消费者)只管取出请求并送乘客到达目的地。

同时,严格以层次化设计要求编写的程序在可扩展性上表现更好。它们各司其职,只有对外接口尽量保持不变。然后,一切的更改都只会影响自己这个模块,可以更少考虑修改后的连锁反应,降低修改成本。

posted @ 2022-04-26 22:50  ChorlingLau  阅读(106)  评论(0编辑  收藏  举报