OODC 2024 如约而至。
欢迎来到 @Monterey 官方渠道
观看 OO 你好,博客发布活动。
OODC (Object Oriented Developers Conference),OO 开发者大会的简称,每个课程单元结束时在 CSDN 举行,拥有最新的本单元 IDEA 平台、编程技术、代码架构和产品安全的学习和交流机会。预计共举办四期。本期是第二期。
在上一期 OODC 中,我们用发布会的形式展示了我们的最新研究成果。本期 OODC ,我们的研究团队将从别样的视角,继续为大家带来关于本期内容电梯调度问题的独特理解。
1 引入:三神谜题
先让我们欣赏如下的一个经典问题:
你因为飞船事故来到了一个陌生的外星文明,遇见了三位神明:真神、假神和变化之神。
你想要寻求他们的帮助,但遗憾的是,你无法分辨出这三个神,只能通过问问题来获取信息。
三个神明告诉你,你总共只能问他们三个问题,问哪个神都可以,在问问题的过程中,需要遵循如下规则:
对于每一个问题,三个神只会回答ozo
或者ulu
,这两个词一个是“是”的意思,另一个是“不是”的意思,但由于这是外星语言,你并不知道具体的对应情况。
此外,真神只说真话,假神只说假话,变化之神随机给出答案(也就是没有参考价值)。
现在请问:你该怎么提这三个问题来具体指出每个神的身份,从而获得他们的帮助。
我们假设三个神A、B、C从左到右站在你面前。
我们很容易发现,ozo
和ulu
不确定的语义给我们带来了很大困扰。
事实上,我们需要透过表面规定的束缚,另辟蹊径来思考这个问题。
📌当我们用这样的句式提问:
如果A,你会说ozo
吗?
只要对方不是变化之神,回答ozo
,A事件必定为真;回答ulu
,A事件必定为假。无需考虑ozo
的具体语义,也无需关心其为真神还是假神。
如果一时难以理解,您可以对ozo
的语义、神明的具体身份进行分类讨论,将A换成某个具体的命题代入验证。
突破了这个关键点后,如何提问就变得明晰起来。
- 先问B:如果我跟你说你左边的神是变化之神,你会说
ozo
吗?
B可能是变化之神,或者是真/假神。
B如果回答ozo
,若B是前者,说明C不是变化之神;若是后者,说明你提问的假设为真,C也不是变化之神;同样如果B回答ulu
,那么A就一定不是变化之神。
我们不妨假设B回答ozo
,C一定不是变化之神。
- 问C:如果我说你是真神,你会说
ozo
吗?
C现在不是真神就是假神,回答ozo
就是真神,回答ulu
就是假神。不妨认为C是真神。
- 再问C:如果我说B是变化之神,你会说
ozo
吗?
如果回答ozo
,那么B是变化之神,剩下A是假神;如果回答ulu
,也一样可以得出结论。
恭喜您,提问成功!
可以看到,解开这个谜题的关键,不是死板地按照规则思考如何提问题,纠结于不可确定的语义,不可预知的身份,而是在遵循规则的前提下,对阻碍进行最大化的灵活处理。而这正是本期OODC核心内容电梯策略中电梯运行的优化所要遵循的基本思想——
例如,规则要求我们的电梯在到达一层后至少要经过400ms才能到达下一层。那我们只能“无所事事”地等待400ms吗?我们是否可以利用这400ms给出更高效的创新载客方案呢?我们将在运行策略:Quantum-LOOK中向您介绍。
2 总论:全文展望
2.1 历次作业简介
2.1.1 第五次作业要求
模拟一个电梯系统,楼座内有6部电梯,电梯可以在楼座内1-11层之间运行。
系统从标准输入中读入乘客请求信息(起点层,终点楼层),调度器会根据此时电梯运行情况(电梯所在楼层,运行方向等)将乘客请求合理分配给某部电梯,然后被分配请求的电梯会经过上下行,开关门,乘客进入/离开电梯等动作将乘客从起点层运送到终点层。
请求的输入通过官方提供的输入接口来定时投放请求,输出也通过官方接口输出。
本次作业指定了接某个乘客的固定电梯。
2.1.2 第六次作业要求
在第五次作业的基础上,增加如下迭代要求:
- 不指定接某个乘客的固定电梯,需要自行调度
- 新增
RESET
指令输入和RECEIVE
指令输出
2.1.2.1 RESET
指令
输入格式
- 电梯重置:
[时间戳]RESET-Elevator-电梯ID-满载人数-移动一层的时间(单位s)
输出格式
- 电梯接收到重置请求:
[时间戳]RESET_ACCEPT-电梯ID-满载人数-移动一层的时间(单位s)
(该消息由官方包自动输出,无需自行输出) - 电梯开始重置:
[时间戳]RESET_BEGIN-电梯ID
- 电梯重置完成:
[时间戳]RESET_END-电梯ID
正确性说明
- 未接收到重置指令的电梯不得进行重置,不得修改电梯运行参数,以免造成评测错误。
- 接收到重置指令的电梯必须在两次移动楼层操作内将所有乘客放出,并输出
RESET_BEGIN-电梯ID
开始重置动作。- “在两次移动楼层操作内”指的是在官方包自动输出
RESET_ACCEPT
开始,该部电梯在输出RESET_BEGIN-电梯ID
之前至多输出两条ARRIVE
。 - 请在接收到重置指令后尽快完成重置动作,
RESET_ACCEPT
和RESET_END
之间的时间记为响应时间,其不得超过5s。
- “在两次移动楼层操作内”指的是在官方包自动输出
- 电梯输出
RESET_BEGIN-电梯ID
时必须保证电梯轿厢内没有人,且门是关闭的。 - 输出
RESET_BEGIN-电梯ID
和输出RESET_END-电梯ID
之间电梯不得参与电梯调度,即不能开关门、进出乘客、上下楼层、输出RECEIVE
(见RECEIVE
约束)等,处于静默状态。 - 完成重置动作后,电梯处于刚开始进行重置动作的楼层(原楼层)。
2.1.2.2 RECEIVE
指令
正确性说明
请在乘客进入电梯和电梯移动前输出RECEIVE
来说明乘客请求分配情况。
- 乘客只能在电梯外才可以有相关
RECEIVE
输出(请注意OUT
、IN
和RECEIVE
的输出顺序) - 任何时刻任何一个乘客请求都至多会被分配给一部电梯,乘客只能进入
RECEIVE
输出规定的电梯。 - 电梯内没有乘客且没有
RECEIVE
到某个乘客请求时的移动(输出ARRIVE
)是不合法的移动。
RECEIVE
取消
RECEIVE
被取消当且仅当以下两种情况发生:
- 电梯开始重置后,其之前与此电梯有关的
RECEIVE
全部取消。也即[时间戳]RESET_BEGIN-电梯ID
输出后,之前仍有效的RECEIVE-乘客ID-电梯ID
全部取消,相关乘客处于未分配状态,电梯也视为未RECEIVE
到任何乘客。 - 乘客中途走出电梯后,相应的
RECEIVE
被取消。也即[时间戳]OUT-乘客ID-所在层-电梯ID
输出后,最近一个RECEIVE-乘客ID-电梯ID
被取消。
2.1.3 第七次作业要求
在第六次作业的基础上,增加如下迭代要求:
- 引入新的重置指令要求
- 支持双轿厢电梯运行
2.1.3.1 第二类RESET
指令
第二类重置请求将电梯修改为双轿厢电梯,重置参数包含需要重置的电梯id
,换乘楼层,两个轿厢的相关参数(移动一层的时间和满载人数)相同。
输入格式为:[时间戳]RESET-DCElevator-电梯ID-换乘楼层-满载人数-移动一层的时间(单位s)
当重置完成后,轿厢 A 默认初始在换乘楼层的下面一层,轿厢B默认初始换乘楼层的上面一层。
两种重置操作的区别仅在于结果(重置操作完成后的影响),完成过程、输出格式及与RECEIVE
的约束关系与第六次作业完全相同。
2.1.3.2 双轿厢电梯
双轿厢电梯是指在同一电梯井道内同时拥有两个独立的电梯轿厢,而电梯系统默认的普通电梯是指在一个电梯井道内只有一个轿厢。为了保证两个轿厢不相互碰撞,将楼层分为上区、下区、换乘楼层,其中上区为换乘楼层以上的所有楼层,下区为换乘楼层以下的楼层,均不包含换乘楼层。在整个运行过程中,要求轿厢 A 只能在下区和换乘楼层运行,轿厢 B 只能在上区和换乘楼层运行,同一井道内的两轿厢不能同时位于换乘楼层。
2.2 全文术语解释
本文涉及较多的概念名词,在此统一解释。
📌理解这些概念对于完成本单元有很大启发。
2.2.1 候乘表
候乘表由乘客请求组成,分为总候乘表和分候乘表。总候乘表只有一份,记录所有的乘客请求;分候乘表再分为一级和二级,一级分候乘表每个电梯持有一份,它们独立地完成各自分候乘表上的所有乘客请求。二级分候乘表由一个电梯的两个轿厢各自持有一份。两个轿厢也独立地完成各自分候乘表上的所有乘客请求。
2.2.2 运行策略
指单个电梯处理自己分候乘表中所有乘客请求采取的策略,研究团队提出了Quantum-LOOK策略。详见运行策略:Quantum-LOOK。
2.2.3 调度策略
指总候乘表的乘客请求经过调度器全部分配到各电梯一级分候乘表所用的策略,在本设计中,调度器不直接操作轿厢的二级分候乘表。研究团队提出了幽灵电梯策略。详见调度策略:幽灵电梯。
2.2.4 外视角、内视角
这是为了帮助读者更好理解上述的运行策略提出的辅助概念。外视角是指通过乘客视角审视整个电梯系统得到的结果,而内视角是指创造电梯的编程者们所看到电梯实际运行逻辑。
2.2.5 门时段、移动时段
指导书规定,电梯开门需要200ms,关门需要200ms。同一次开门和关门之间的间隔是门时段。
相似地,电梯到达一层后、关门后或者重置结束后和到达下一层之间的间隔是移动时段。
3 新知:同步与锁
3.1 锁的选择与设置
三次作业中,研究团队共使用了两种形式的java
多线程锁。
3.1.1 synchronized
同步块
这是一种入门级锁机制,在三次作业的代码中,研究团队大多采用该机制。
- 对于行数不多的需要加锁的方法,比如向总候乘表中加入一个请求,从总候乘表中移除一个请求等等,可以用
synchronized
直接修饰该方法,如果该方法是对象方法,那么锁就是调用该方法的对象;如果该方法是静态方法,那么锁就是方法所在的类 - 对于逻辑流程复杂、仅部分操作需要加锁的方法,应该用
synchronized
包裹需要加锁的代码块,比如电梯在执行重置操作时可能需要将未完成的乘客请求放回总候乘表。只需要对放回的操作加锁即可
加锁是为了保证线程安全,以总候乘表为例,调度器线程、输入线程、电梯线程需要频繁地读写总候乘表,如果不对读写操作加锁,很容易出现遍历时删除等等安全问题。
但加锁并不是越多越好,过多的非必要锁会使线程死锁的概率大大增加,同时也让执行时间增加,性能下降。
3.1.2 自定义换乘锁——防止轿厢碰撞
一般而言在多线程编程中,只允许单个线程对临界资源进行访问。
比如在第七次作业中,要求双轿厢电梯的两个轿厢不能同时进入换乘层。换乘层的进入许可就可以被理解为一种临界资源。显然,这就要求我们为其设计一种锁结构,确保其能被安全访问。
我们可以考虑借鉴Semaphore
信号量机制来控制这些临界资源。
初始化一个Semaphore
对象时一般会用一个整数
n
n
n指定它的许可数量,即表示有多少个可供访问的同种临界资源。
在使用该对象时,用其提供的acquire
方法尝试获取一个许可,如果获取成功,那么该线程可以继续访问临界区,并且Semaphore
对象的许可数量减1;如果Semaphore
对象已经没有剩余的许可,则获取失败,该线程被挂起,直到其他线程通过release
方法释放许可。因此,请确保在临界区访问完毕后释放许可。
我们可以将换乘层抽象为一个TransferLock
(自定义类)对象,每个电梯拥有一个该对象,并由该电梯的两个轿厢共享这两个对象。下面是具体的实现细节:
- 成员变量
isLocked
:私有的boolean
值,初始值为false
,表示许可,数量为1。每次需要进入换乘层时先申请许可(将该值设为true
);离开换乘层后立即释放许可(将该值设为false
)。 - 方法
acquire
:如果许可处于上锁状态,则执行wait
方法将该线程休眠,否则表示可以获得许可,给许可上锁后退出 - 方法
release
:将许可解锁,并使用notifyAll
方法唤醒所有在该对象上休眠的线程
3.2 锁和同步块内语句的关系
锁对其对应同步块内的语句起到了规范的作用。
以synchronized
同步块为例,想要进入该同步块必须持有对应的锁,拿到对应的锁后,只有退出同步块才会释放这个唯一的锁。
因此在同步块内的语句被执行时,锁保证了当前进程不会被切换,语句流严格地由同一线程顺序地执行,规范了线程的行为,这也是锁的本质作用。
4 架构:层次设计
📌由于是完全迭代开发,UML类图与类功能简析均以最后一次作业为例。
4.1 UML类图
4.2 类功能简析
4.2.1 main包
Main
类:主线程,负责初始化数据并启动一系列其他工作线程。
4.2.2 io 包
InputThread
类:输入线程,配合官方输入接口使用,负责读取乘客请求,并将其加入总候乘表。在输入结束时,向总侯乘表发出结束信号。OutputHandler
类:输出处理器,配合官方输出接口使用,输出规定格式的相关信息。
4.2.3 system 包
MyRequest
类:单次乘客请求,是WaitingTable
的基本组成部分。MyResetRequest
类:单次电梯重置请求,第六次作业新增输入。MyDoubleCarResetRequest
类:单次电梯第二类重置请求,第七次作业新增输入。WaitingTable
类:乘客候乘表,将实例化出总候乘表和分候乘表。Elevator
类:电梯线程,负责实际接送乘客。Car
类:轿厢线程,是电梯的组成部分。当某个电梯完成第二类重置请求后,实质上转为由A、B两个轿厢独立运行,行使职能。电梯仅负责将自己一级分候乘表的乘客请求分发给两个轿厢的二级分候乘表。Adviser
类:电梯/轿厢顾问,电梯/轿厢的组成部分之一,负责为其提供下一步指令。该类体现的是电梯/轿厢的运行策略。Scheduler
类:调度器线程,负责把总候乘表的请求分配到各个电梯的一级分候乘表。该类体现的是电梯的调度策略。GhostElevator
类:幽灵电梯,对某一特定时刻电梯线程或者是轿厢线程的复刻,拥有与其完全一致的状态属性,独立的策略属性和略微改动的运行方法,是非线程类。详见调度策略:幽灵电梯。TransferLock
类:自定义的换乘层进入许可锁。
4.2.4 util包
Order
类:枚举类,枚举所有的指令。共有:END
,TELEPORTATION
,OPEN
,WAIT
,TRANSFER
,TURN
,RESET
七种指令。分别对应:结束运行、瞬移(移动)、开门、等待、换乘、调头、重置。ElevatorArgs
类:电梯的初始静态指标参数、全局重置统计参数和全局换乘统计参数。
4.3 未来扩展
我们主要考虑如下的迭代可能:
- 如果有新的调度策略要求,或者引入了新的性能指标,我们可以很轻松地在调度器类
Scheduler
中换用不同的调度方法 - 如果有新的输入指令,如“第三类重置指令”,我们可以新增对应的请求类,在
Order
中新增对应的指令,并在Adviser
中补充相应的处理逻辑 - 如果未来能找到更高效地电梯运行策略,也可以在
Adviser
中换用运行策略方法而不影响到电梯Elevator
本身的动作。
5 流程:线程协作
下面是一个简单的流程示意图。
6 核心:电梯策略
6.1 运行策略:Quantum-LOOK
这是一种基于LOOK算法进行改进的策略,研究团队基于过往项目经验提出了这一理论。理解这一理论的核心在于理解其量子性。下面我们将为您详细介绍。
📌在探讨电梯的运行策略时,我们并不关心电梯的调度策略,因为这是两个完全不同的概念。因此为了让说明更加简洁,在本节中,我们在第五次作业,也就是直接指定处理某个乘客请求的电梯这一情景下进行论述。
另外,该策略不仅可以用于单个电梯,也可以用于电梯的两个轿厢。
6.1.1 LOOK
先来了解基本的LOOK算法原理。
- 初始化时为电梯规定一个初始方向,表明电梯将沿着该方向移动。
- 到达某楼层时,首先判断是否需要开门:
- 如果发现电梯里有人可以出电梯(必须是到达目的地),则开门让乘客出去;
- 如果发现该楼层中有人想上电梯,并且电梯容量未满、目的地方向和电梯方向相同,则开门让这个乘客进入。
- 接下来,进一步判断电梯里是否有人。
- 如果电梯里还有人,则沿着当前方向移动到下一层。
- 否则,检查分候乘表中是否还有请求:
- 如果分候乘表不为空,且某请求的发出地是电梯"前方"的某楼层(不含本层),则电梯继续沿着原来的方向移动。
- 如果分候乘表不为空,且所有请求的发出地都在电梯"后方"的楼层上,则电梯调头。
- 如果分候乘表为空,且输入线程没有发出结束信号,则电梯停在该楼层等待。
- 如果分候乘表为空,且输入线程已经结束,则电梯线程结束。
6.1.2 Quantum
请允许我们为您隆重介绍Quantum。
Quantum,许多业界人士也称其为量子电梯。我们也同样接受这一说法,并会在下面的解释中引用它。
📌量子力学领域的研究指出,实物粒子在没有被观测的时候处于量子态,而被观测后发生塌缩。
📌评测机只关注每行的电梯输出信息,如到达某层的一瞬间、门刚打开的一瞬间和完全关闭的一瞬间。它并不关注电梯在其他时间的运行状态。
根据以上信息,我们可以很容易地把评测机对输出信息的每次判定看作是对电梯这一实物的一次观测。这种观测有五种结果,也分别对应规定的五种输出形式:
- 电梯到达某一位置:
[时间戳]ARRIVE-所在层-电梯ID
- 电梯开始开门:
[时间戳]OPEN-所在层-电梯ID
- 电梯完成关门:
[时间戳]CLOSE-所在层-电梯ID
- 乘客进入电梯:
[时间戳]IN-乘客ID-所在层-电梯ID
- 乘客离开电梯:
[时间戳]OUT-乘客ID-所在层-电梯ID
其中,乘客进入和乘客离开两个动作没有时间上的持续性和动作上的连贯性,不是我们关注的重点。
其余三种情况的观测规则(评测规定)如下:
- 电梯开门需要200ms,关门需要200ms。
事实上,观测只发生在开门开始时刻和关门完成时刻,因此我们可以得出如下定理:
📌定理一:对于一组开关门动作,电梯关门事件必须发生在电梯开门事件的400ms及以后
另一条观测规则是:
- 电梯移动需要400ms。
同样地,我们可以得出对应定理:
📌定理二:电梯移动这一事件必须发生在上一个关门、移动或者重置事件的400ms及以后
符合这两条定理的电梯运动都是符合规定的。这两条定理所描述的重要观测事件构成了两个关键时段:
开关门间隔400ms(门时段)和楼层间运行400ms(移动时段)。
我们的Quantum量子优化策略正是对这两个时段电梯的状态进行改进。
在本设计中,电梯线程运行时的每次循环都让Adviser
电梯顾问基于当前情况给出六种指令(换乘指令是轿厢独有的指令,在此暂不涉及)的一种,然后电梯执行这一指令,也就是问询→执行构成一次基本的运行循环。
那么,在这样的语境下,Quantum策略就是最大化地利用电梯线程在执行阶段必须经历的两个关键时段,它很好地贯彻了我们在引入:三神谜题最后提出的优化核心思想。
它遵循了规则,保证每次观测的正确性,又利用了规则,两次观测之间的状态是可以自定义的。
我们由此对应提出了三种具体的优化方式:
- 基于门时段最后时刻问询的极限载客
- 基于移动时段休眠时间精确缩减的弹射起步
- 基于移动时段最后时刻问询的瞬间移动
下面逐个进行介绍。
6.1.2.1 极限载客
先谈第一个优化细节——极限载客。
📌在本设计中,一组完整的开关门动作包括:问询阶段
Adviser
检查分候乘表判断是否开门、执行阶段执行门时段400ms必要休眠。
因此,在400ms休眠结束时,可能分候乘表中加入了新的请求,而Adviser
本次并不能看到这个新请求。按照正常的流程,此时电梯应该关门并移动。
极限载客的优化是:在400ms结束瞬间时刻,再次问询Adviser
,如果有新请求并且有条件接客,那么保持开门直到乘客进入。
此时在外视角看来就是电梯已经恰好关门,而在内视角看来,电梯仍有能力瞬间接到乘客,门显然还是开着的。
也就是说,电梯在门时段末期处于门开着和门即将关上的量子叠加态。
6.1.2.2 弹射起步
电梯的行为是电梯的行为,线程的行为是线程的行为。等待是线程的行为,不是电梯的行为。
我们所谈到的移动时段指的是到达某一层楼、在某层关门或者在某层完成重置与到达下一层楼的时间间隔。在某一楼层电梯陷入等待是一种“额外”的行为。
我们设想这个场景:
- 在初始时,1号电梯位于1层,1000ms时刻,第一个请求到来,1号电梯被分配前往2层接客。
我们假设1号电梯即刻响应,也就是1000ms时刻,1号电梯结束等待(启动),经过移动时段休眠400ms到达2层并输出ARRIVE。但是真的需要这样吗?
“聪明”的1号量子电梯可以这么做:在600ms时刻,“预判”这一请求,先行启动,在1000ms时刻和新请求同时到达2层。但在具体实现上,仍然是在1000ms时刻结束线程等待,然后不经休眠,直接输出ARRIVE表示到达2层。
在外视角看来,电梯在这600ms时刻到1000ms时刻的时段当然是处于等待状态,因为没有观测表明电梯在运动。而从内视角看,电梯是提前开始移动了的,而不是还在休眠等待。
总结来说,在有线程等待行为的移动时段内,线程等待一开始,电梯就处于暂停等待和移动的量子叠加态,直至等待结束。
这一量子叠加态表现出极其明显的可观察特征——电梯弹射起步。这一现象常常发生在初始时,由于初始时第一条请求到达时刻往往超过400ms,这也是优化后提升最显著的部分。
想要完成这一优化,正确的做法是在电梯中维护一个属性,用来记录每个移动时段的终止也是下一移动时段的开始时刻。即每次运行循环到达、关门或者是重置完成的时刻(要选取最新的记录),然后在移动的时候判断还需要移动多少秒,而不是固定的400ms。
📌在后续的迭代中,为保证正确性,当出现新的关门、移动等动作时,切记更新该属性。
6.1.2.3 瞬间移动
我们再设想如下场景:
- 输入线程带来了一系列输入,它们恰好有6条,第一条在1000ms时刻到达,每条间隔200ms,内容是6个乘客均要求1号电梯从2层将其运送到6层。
我们假设所有电梯已经按照前面提出的优化方案进行了升级。
那么1号电梯在1000ms时输出ARRIVE到达2层,瞬间开门,接入第1个乘客,1400ms时刻,基于极限载客,再次问询,保证能够接到第2、3个乘客,然后关门,休眠等待400ms后出发。
但是我们很容易注意到,电梯完全将6名乘客都接走,这样就不用再次调头返回。因此我们可以进行如下改进:
在移动时段最后时刻,再次问询Adviser
是否开门,如果得到开门指令,那么执行开门指令,然后立即结束所在的移动这个指令——从执行阶段回到了问询阶段。
回到上述场景中,再次问询时第4、5个乘客已到达,电梯开门将其接入,然后回退来到问询阶段,这一次电梯顾问给出的无论是开门指令还是移动指令,肯定能把第6个乘客再次接入。
从外视角看,移动时段电梯在两层楼之间运动,而从内视角看,电梯还在本层等人,也就是在移动时段的任何非线程等待期间(电梯有接乘客的能力),电梯处于移动和准备开门的量子叠加态。
这一量子叠加态也表现出可观察特征——“瞬移”到下一层。相比弹射起步而言,由于大部分时间电梯线程都在工作,瞬移更具有代表性。
这也是研究团队将MOVE
指令改为TELEPORTATION
的原因——希望每次都能发生这种令人惊叹的瞬移。
6.1.3 实际表现
// input-Homework5 only
[1.0]1-FROM-1-TO-6-BY-2
[1.3]2-FROM-1-TO-6-BY-2
[1.6]3-FROM-1-TO-6-BY-2
[1.9]4-FROM-1-TO-6-BY-2
[2.2]5-FROM-1-TO-6-BY-2
[2.6]6-FROM-1-TO-6-BY-2
// output
[ 1.0460]OPEN-1-2
[ 1.0460]IN-1-1-2
[ 1.4470]IN-2-1-2
[ 1.4470]CLOSE-1-2
[ 1.8620]OPEN-1-2
[ 1.8620]IN-3-1-2
[ 2.2620]IN-4-1-2
[ 2.2620]IN-5-1-2
[ 2.2620]CLOSE-1-2
[ 2.6640]OPEN-1-2
[ 2.6640]IN-6-1-2
[ 3.0790]CLOSE-1-2
[ 3.4840]ARRIVE-2-2
[ 3.8920]ARRIVE-3-2
[ 4.2930]ARRIVE-4-2
[ 4.6950]ARRIVE-5-2
[ 5.0970]ARRIVE-6-2
[ 5.0970]OPEN-6-2
[ 5.0970]OUT-1-6-2
[ 5.0980]OUT-2-6-2
[ 5.0980]OUT-3-6-2
[ 5.0980]OUT-4-6-2
[ 5.0980]OUT-5-6-2
[ 5.0980]OUT-6-6-2
[ 5.4970]CLOSE-6-2
6.1.4 重置与Quantum-LOOK
对于第一类和第二类重置请求:
重置结束的效应和关门或是到达某一层的效应是一样的,对于弹射起步优化,需要更新对应的记录属性(记录每个移动时段的终止也是下一移动时段的开始时刻)。
这一点在前面的部分也已经有强调。
6.2 调度策略:幽灵电梯
6.2.1 实现思路
在第五次作业中,由于指定了接某个乘客的固定电梯,调度策略在第六次作业及以后才真正有效地投入使用。当然,出于统一性考虑,我们将简单地匹配id
也视为一种调度策略。
所谓调度策略是指:调度器每次接到一个新的请求,要把它分给哪个电梯?也就是它是一个接受MyRequest
类型,返回电梯id
(int
类型)的方法。
幽灵电梯事实上是一种局部最优原理的白箱调度。
我们以6个电梯为例,当调度器接收到一个新请求a
,进行如下操作:
- 基于当前时刻,复刻出 6 ∗ 6 6*6 6∗6共 36 36 36个幽灵电梯:这些幽灵电梯6个一组,每组的6个幽灵电梯分别由当前时刻 1-6 号电梯复刻而来。 复刻指的是它们拥有完全一样的状态属性、独立的策略属性和略微改动的方法。
📌状态属性:
如电梯各项速度参数、容量参数、当前所处楼层。分候乘表等;这是为了达到完全模拟的效果。
策略属性:
如电梯顾问是策略属性,它只负责根据电梯的分候乘表驱动电梯运行,应该是独立的。
📌略微改动的方法是指:
仍然按照问询→执行的流程运行,但是它不是线程,直接通过act
方法启动。一次性使用;
不执行休眠,而是将休眠的时间累计;
无需输出信息;
它的分候乘表不会新增请求,表空后没有等待,直接结束运行;
最后结束时返回运行时间。
- 将新请求
a
分别加入6组,规则是:加入第i
组的i
号幽灵电梯。 - 启动各组幽灵电梯,直至它们完成各自的所有分候乘表请求而结束。
- 计算每一组的运行时间——每组那个运行最久幽灵电梯的运行时间是该组的运行时间。
- 选出运行时间最短的组,假设是第1组,那么最终调度器将新请求分给1号电梯。
简单来说,就是调度器模拟出把新请求分给不同电梯的全部情况,算出所需时间,最终选中那个预期时间最短的。这里的6组就是6种不同的情况。
该调度策略以总运行时间为主要指标,但在实际检验中,出于运行时间考量的调度也使得各个电梯的非必要移动、开关门等动作大大减少,在耗电量方面也有很大的优势。
6.2.2 重置与幽灵电梯
对于第一类重置请求和第二类重置请求:
- 只有不处于重置的电梯才能参与调度,重置后电梯重新参与调度
- 若某一时刻有超过3台电梯在重置,将这个新请求重新放回总候乘表,稍后分配。
此外,对于经过第二次重置后的双轿厢电梯,研究团队提出了不同的模拟办法:
- 双轿厢电梯的幽灵电梯相关数据,不再来自于电梯本身的复刻,因为实际上此时是电梯的轿厢线程在运行,因此它来自于轿厢的复刻,由于轿厢与普通电梯的相似性,只需要扩展幽灵电梯的构造方法即可
- 调度器会判断新请求会发给轿厢A还是轿厢B,由此用轿厢A或轿厢B单个轿厢来进行复刻
- 针对复刻时的乘客请求,需要特别处理:对于那些需要换乘的请求,将其目的地直接改为换乘层。
对于上述第三点的处理,我们做出如下的解释:
- 首先,单个轿厢本身就无法完整地处理需要换乘的请求,但是本设计中,它会接到这种请求,在换乘层将其重构后再次放出,因此只需要模拟其到达换乘层的过程即可
- 其次,一个需要换乘的请求,往往也是跨楼层数多的请求,由于改换了目的地,双轿厢电梯的模拟时间相较于普通电梯有较大的优势(运行距离减短),调度器更倾向于将其分配给双轿厢电梯。而双轿厢电梯本身有耗电量的巨大优势,这样可以进一步平衡运行时间指标和耗电量指标,对性能提升也有较大帮助。
7 迭代:变与不变
📌 在历次迭代中,不变的是电梯的运行策略和调度策略(见上),变的是每次作业的新增处理请求。
7.1 第六次作业
7.1.1 RESET
指令
7.1.1.1 新处理
处理流程参考如下:
- 当输入线程识别到
RESET
类型指令时,读取进行重置的电梯id
,将该电梯对应属性设置为“正在重置”,表示开始重置; - 电梯顾问优先检查该属性,发现该属性为
true
后,指导电梯在执行阶段执行重置 - 电梯执行重置指令时,需要完成如下事件:将当前乘客全部放出,未到达乘客需要重构请求,重新加入总候乘表;当前未完成的分候乘表请求全部清空,重新加入总候乘表;重置容量属性与速度属性。
- 休眠要求时间后,重置完成。
7.1.1.2 补丁处理
我们需要设置两处地方以防出现错误:
- 设置全局重置统计参数
r
,初值为0。当输入线程读取到一个重置请求,该值加1;当正在重置的电梯将当前乘客请求、分候乘表未处理请求全部放回总候乘表后,该值减1。 - 修改电梯线程结束条件和调度器线程结束条件:
r==0
(所有因重置而未处理的请求至少已经重新全部加入总候乘表,确保“最后一批乘客请求“仍能得到处理)是必要的新增条件。
7.1.2 RECEIVE
指令
目的是鼓励使用调度器而不是自由竞争。
对于本架构设计,只需当某个请求经调度器分配时先输出相关信息再分配即可。
📌关于输出的取消问题,只需要将上一部分做好即可——将分候乘表请求与当前处理请求放回这个动作就是
RECEIVE
取消的标志。这些请求稍后就将由调度器再次分配而再次输出RECEIVE
。
📌注意,由于输出
RESET_BEGIN
后之前的所有RECEIVE
才取消,所以要保证放回请求的操作在输出RESET_BEGIN
之后进行。
7.2 第七次作业
📌本次作业的核心是支持双轿厢电梯的运行。
7.2.1 新处理
对于第二类重置请求,处理流程参考如下:
- 当输入线程识别到第二类
RESET
类型指令时,读取进行重置的电梯id
,将该电梯对应属性设置为“正在重置”,表示开始重置; - 电梯顾问优先检查该属性,发现该属性为
true
后,指导电梯在执行阶段执行重置 - 电梯执行重置指令时,需要完成如下事件:将当前乘客全部放出,未到达乘客需要重构请求,重新加入总候乘表;当前未完成的分候乘表请求全部清空,重新加入总候乘表
- 休眠要求时间后,重置完成。
- 启动两个轿厢线程,并给其传递必要的参数,如容量、换乘楼层等
- 电梯本身转入新循环运行,不再进行问询→执行的基本循环,而是只负责为两个轿厢不断地传递乘客请求任务(事实上两个独立的轿厢仍按问询→执行不断执行)
7.2.1.1 第二类重置后的电梯
电梯在进行第二类重置后,实际上由轿厢线程来完成任务。
电梯由原来的问询→执行循环逻辑变为分发任务逻辑。
调度器不直接将请求发送至轿厢的二级分候乘表,而是依旧发送至电梯的一级分候乘表。电梯的分发任务逻辑不断地从一级分候乘表中取出请求,按照如下规则分发:
- 若出发地位于下区,分发给A轿厢
- 若出发地位于上区,分发给B轿厢
- 若出发地位于换乘层,目的地在上区的分发给A轿厢,目的地在下区的分发给B轿厢
- 当调度器不再发送新请求,且一级分候乘表为空,结束分发
结束分发后,电梯分发任务逻辑停止运行。
7.2.1.2 轿厢线程
轿厢线程是作为电梯的组成部分来运行的。
轿厢线程仅在对应的电梯完成第二类重置请求后启动,和执行第二类重置前的电梯基本类似,也按照Quantum-LOOK策略来运行,同样拥有自己的轿厢顾问,之所以将其设计为不同的一个类,是出于对其换乘要求的考虑。
具体而言,轿厢线程有下面这些新的设计:
- 复用
Adviser
类作为顾问,但是通过新的方法来获得指令,新增了换乘这一指令:轿厢进入换乘层,如果有需要换乘的乘客请求,将其放出,重构乘客请求,放回总候乘表。 - 同一电梯的两个轿厢线程共享一把锁,某个轿厢每当尝试进入换乘层时,需要先获取锁,离开换乘层后,释放该锁。锁机制通过自定义锁结构简洁实现。
- 不允许轿厢在换乘层结束运行或者等待,每当轿厢在换乘层达到结束或者等待的条件时,先离开换乘层,再结束或者等待。
实际上,轿厢就是一台只能在在上区和换乘层,或者下区和换乘层运行的迷你电梯。
7.2.2 补丁处理
我们需要设置两处地方以防出现错误:
- 设置全局换乘统计参数
t
,初值为0。当调度器确认将某个新请求发给双轿厢电梯,且该请求需要经过换乘才能完成时,该值加1;当某个轿厢执行换乘指令,每放出一个需要换乘的乘客请求后,该值减1。 - 修改电梯线程结束条件和调度器线程结束条件:
t==0
(所有因换乘而重构的请求至少已经重新全部加入总候乘表)是必要的新增条件。
8 修复:查缺补漏
8.1 发现漏洞
调试是编程者所需要掌握的必备技能之一。
多线程程序的调试与普通单线程程序的调试有较大不同。如果只是简单地添加断点,会改变线程的执行逻辑,很容易导致错误无法复现等一系列问题。
因此在三次作业中,我们全程采用打印输出策略来进行调试。
例如,在电梯线程问询或者执行的关键节点,增加打印语句,来判断当前电梯线程是否能正常结束;或者在调度器线程中添加打印语句,检查调度分配的逻辑是否有误。
我们可以直接使用System.out
来打印输出,也可以配合每次作业提供的官方输出包,加上时间戳来输出。打印输出既能使我们了解多线程执行时的信息变化,又能避免影响多线程执行逻辑,是多线程调试的必备选择。
8.2 Five-Reset 攻击
研究团队遇到了著名的Five-Reset 攻击, 该攻击出现于第六次作业互测期间。
第六次作业中,原本的幽灵电梯调度策略为:
- 无条件优先给不在重置的电梯分发请求
这一方案带来的问题是,如果在某个时刻五台电梯均在重置,便会导致剩下的一台电梯被派发所有的请求,导致运行时间过长。
我们为此提出了解决方案:
- 若某一时刻有超过3台电梯在重置,将这个新请求重新放回总候乘表,稍后分配
9 心得:终章落幕
📌第二单元的学习步入了尾声,我们将从下面几个方面做出总结。
9.1 线程安全
多线程的线程安全主要有两方面的挑战。一方面,需要我们深刻理解多线程之间的协作;另一方面,多线程的运行结果难以复现,调试和优化都比较困难。我们可以通过以下方法保障我们的线程安全:
- 采用成熟的多线程设计模式。生产者-消费者模式作为比较简单的多线程模式,可以大大降低了本次作业的难度。
- 多做测试。例如模拟高并发时的情景。
- 可以画出协作图帮助理解,意识到哪里会可能出问题,哪个方法需要加锁,哪里可能出现死锁等等。
9.2 层次化设计
9.2.1 设计模式
本单元作业中研究团队采用了策略模式,调度器在不同次作业应用不同的调度策略,大大降低了类与类之间的耦合,保证了程序良好的可扩展性。
9.2.2 优化设计
由于性能分的评分标准比较全面,实际上全局优化是较为困难的,更多的是一次次的局部优化,我们采用的幽灵电梯调度策略在等待时间和总运行时间表现良好,而耗电量由于没有进行明确的评估,可能没有做到最完美的优化。