301
社区成员
发帖
与我相关
我的任务
分享OO Unit2主题为模拟多线程实时电梯系统,涉及线程协作、同步与锁的使用、调度架构设计等内容
三次作业架构上是逐级迭代的,基本上第一次架构决定了整个单元的框架,所以直接从HW5开始讲起,后面HW6、HW7逐渐新增内容。


整体上参考了指导书和往年学长博客,采用了 输入线程-调度线程-电梯线程 结构,线程之间采用 共享类 进行同步, 对 共享类 的访问均采用 sychronized 块,保证块内操作的原子性与可见性,同时在 sychronized 块内均使用 notifyAll() 方法防止死锁。这是基本稳定的架构内容,变化的内容会在每个版块中介绍。
在程序开始时,首先由 主线程 启动 输入线程 和 调度线程 ,而 调度线程 在被启动后,启动六个 电梯线程 。
对于 输入线程 ,其任务为接收并包装主线程的输入(官方包已经提供),提取出 Request 类交给 调度线程 处理。两个线程之间通过共享 WaitingRequests 类进行协作,采取 生产者-消费者 模式, 输入线程 生产请求 Request 给 WaitingRequests ,而 调度线程 消费 WaitingRequests 中的 Request 。
对于 调度线程 ,其任务为决定哪个 电梯线程 应该处理哪一个 Request 。如上所述,作为 WaitingRequests 的消费者, 调度线程 从中获取需要进行分配的 Request 。
为了防止 WaitingRequests 为空时 调度线程 轮询浪费CPU时间,当 WaitingRequests 为空且 输入线程 未中止时, 调度线程 会调用 WaitingRequests.wait() 方法释放锁并休眠,直到被 输入线程 调用 WaitingRequests.notifyAll() 方法唤醒,检查 WaitingRequests 是否仍为空。如果仍未空则重新获取 Request ,不为空则进行调度。
同时, 调度线程 和 电梯线程 之间通过 ElevatorRequests 进行共享, 每个 电梯线程 都有自己对应的 ElevatorRequests ,而 调度线程 可以访问这六个 ElevatorRequests 中的任意一个。 调度线程 和 电梯线程 仍采用 生产者-消费者 模式,但生产者是 调度线程 ,消费者是 电梯线程 。当 调度线程 向某一个 电梯线程 的 ElevatorRequests 生产了一个 Request ,说明该 Request 被 调度线程 分配给了该 电梯线程 。
在 调度线程 的 分配策略 上,我选择了指导书提供的算法。基本逻辑是,第一个 Request 分配给第一个 电梯线程 ,第二个 Request 分配给第二个 电梯线程 ,直到第六个 Request 分配给第六个 电梯线程 。然后将第七个 Request 分配给第一个 电梯线程 ,第八个 Request 分配给第一个 电梯线程 ,以此类推。
这种分配方法的优点在于实现简单,各个 电梯线程 之间负载均匀,不容易出现一个 电梯线程 承载所有 Request 的情况。缺点是无法根据 电梯线程 当前的运行状况分配 Request ,利用电梯可以捎带同向 Request 节约时间的特性。
对于 电梯线程 ,其任务为根据其对应的 ElevatorRequests 将所有的 Request 进行处理。具体来说,需要先移动到 Request 起始楼层,按序输出 OPEN-IN-CLOSE ,在移动到 Request 的目标楼层,按序输出 OPEN-OUT-CLOSE ,完成该 Request 的处理。在电梯每次抵达一个新楼层时,都需要输出 ARRIVE 。
为了防止 ElevatorRequests 为空时 电梯线程 轮询浪费CPU时间,当 ElevatorRequests 为空且 调度线程 未中止时, 电梯线程 会调用 ElevatorRequests.wait() 方法释放锁并休眠,直到被 调度线程 调用 ElevatorRequests.notifyAll() 方法唤醒,检查 ElevatorRequests 是否仍为空。如果仍未空则 WAIT 重新读取 ElevatorRequests ,不为空则进行调度。
在 电梯线程 内部储存了一个类 ElevatorInfo 保存电梯的所有属性,包括电梯内的所有 Request 、所在层数、各种运行参数等。内部还储存了一个类 ElevatorStrategy 用于决策电梯下一步应该进行什么操作。
在 电梯线程 实际运行中采取了 状态机 写法。

MOVING-WAITING-WORKING ,有六种动作 UP-DOWN-START-WAIT-OPEN-CLOSE 。内置的 ElevatorStrategy 会根据 ElevatorInfo 判断下一步应采取什么动作。
第一种方法为指导书提供的 ALS ,新增 主请求 和 被捎带请求 两个概念,优先处理 主请求 ,顺路捎带 其他请求 。
$1、$主请求选择规则:
$2、$被捎带请求选择规则:
第二种方法为往届博客和现实中电梯都采用的 LOOK ,新增一个 currentDir 属性
currentDir 方向移动currentDir 仅在以下情况发生时转向currentDir 方向前面的楼层没有人在实现了 ALS 之后,我听说 LOOK 更现实,就尝试了 LOOK ,并且基本上以后都沿用了 LOOK 。
对 输入线程 ,当读到 NULL 就可以中止。中止之前,在 WaitingRequests 中设置 isEnd=true 。
对 调度线程 ,当 WaitingRequests 为空且内部 isEnd=true 时就可以中止。中止之前,在各个 ElevatorRequests 中设置 isEnd=true 。
对 电梯线程 ,当 ElevatorRequests 为空且内部 isEnd=true 且电梯内无人且电梯处于 WAITING 是就可以中止。
在互测阶段没有出现bug,以下bug在中测和本地出现
sychronized 的对象sychronzied 方法共享类START 和 WAIT 状态之间循环ElevatorStrategy 内在 WAITING 时判断是否 OPENWAITINGOPEN-CLOSE 循环 ElevatorStrategy 发现要进人-> OPEN -> 没进人 -> CLOSEElevatorStrategy 里只要 电梯不满 & 该层上梯 就开门 但是 上客 过程中只捎带 同向ElevatorStrategy 应该比 上客 条件更严格/一致MOVING-WAITING 循环ElevatorStrategy 中分析 MOVING 时首先考虑是否有人到达目标楼层 应返回 OPENElevatorStrategy 在 MOVING 里遗漏了开门捎带同向ElevatorStrategy 在 MOVING 里遗漏了开门转向:电梯空 & 没有可捎带请求 & currentDir 方向前面的楼层没有人OPEN-CLOSE 循环ElevatorStrategy 在 WAITING 判断 OPEN 时需要保证该层有反向请求使用状态机让电梯的动作和逻辑分的很清晰,修改起来很方便, 但是也将电梯的判断逻辑分散了,容易因为不一致出现死循环。


HW6新增了 RESET 请求和 RECEIVE 输出。
RESET 请求需要尽快处理,处理前电梯里的人需要赶出电梯重新 RECEIVE ,处理时 调度线程 不能向该电梯调度。
RECEIVE 需要在 调度线程 调度 Request 给 电梯线程 时输出。一个乘客只能分配给一个电梯。RESET 可以使得 RECEIVE 中途取消。 电梯必须先收到 RECEIVE 再开始移动。
因为 RESET 请求需要尽快处理,我担心传给 调度线程 再传给 电梯线程 会赶不上,就决定先在 WaitingRequests 中标记该 电梯线程 正在 RESET 中,然后在 ElevatorRequests 中传递 RESET 请求。
在HW6 调度线程 不能向正在 RESET 的电梯调度,所以在调度之前需要根据 WaitingRequests 中的标记判断该电梯是否在 RESET ,如果在 RESET 则需要换一个电梯调度。
由于在HW6中 电梯线程 在 RESET 之前需要出人,所以需要向 WaitingRequests 中生产 Request 。在重置结束后,还需要向 WaitingRequests 中标记该电梯重置结束。
对 调度线程 ,判断中止的时候还需要判断是否有电梯在重置。
在互测阶段出现了一个bug
RESET 时高并发会RTLE调度线程 将所有 Request 都调度给了剩下的唯一一个电梯。调度线程 发现一个电梯在 RESET 时,让 调度线程 调用 sleep() 休眠一段时间。其余bug均在本地调试
OPEN 与 CLOSE 同时输出RESET 时赶人出门也需要间隔一段时间sleep() 方法RESET_BEGIN 时,已经有另一电梯 RECEIVE 该电梯内乘客RESET_BEGIN 再 RECEIVE 被赶出的乘客ElevatorRequestsRESET_BEGINElevatorRequests 的所有人送回 WaitingRequestsLOOK 在 Strategy 的 WAITING 和 WORKING 时考虑转向WORKING 要电梯无人才能考虑转向WaitingRequest 内记录电梯是否在 RESET 并更改 Scheduler 中止条件Scheduler 调度时应先输出 RECEIVE 在进行调度操作

新增了 双子电梯 ,在同一电梯井内运行,指定一个 换乘楼层 ,两个电梯只能在 换乘楼层 之上或之下运行,且不会接到重置请求。
当受到 双重重置 请求时, 电梯线程 会创建两个特殊的 双子电梯线程 ,这两个电梯仍共享同一个 ElevatorRequests ,仍然在 WaitingRequests 中被认为是一个 电梯线程 。
接收到 双重重置 请求时,先在 WaitingRequests 中标记该 电梯线程 正在 RESET 中,然后在 ElevatorRequests 中传递 RESET 请求。
当 调度线程 接收到 双重重置 请求时需要创建出对应的两个 双子电梯线程 ,替代原来的 电梯线程 。在进行调度时,仍然当做是一个电梯进行调度。
对于 双子电梯线程 ,由于两个电梯的运行楼层有限,故在接收请求的时候,在 ElevatorInfo 中储存被分配到的请求 RealRequests ,而电梯运行则依照经过处理的请求 DealtRequests 。处理过的请求会被限制在当前电梯可运行的楼层范围内,并在 IN-OUT 时判断该 Request 是真正到达了目标楼层,还是仅仅到达了 换乘楼层 。如果请求还没有到达目标楼层,则会交给另一个 双子电梯线程 处理。
为了避免相撞,在 ElevatorRequests 储存一个变量 ifOccpuied ,表示是否有电梯在 换乘楼层 。当有电梯要进入 换乘楼层 ,会先检查该变量 ifOccpuied ,如果结果为 true 则等待,直到变成 false ,将其置为 true 再进入该楼层。当有电梯要离开 换乘楼层 时,先离开换乘楼层,再将其置为 false 。
由于 双子电梯线程 不会向 WaitingRequests 生产 Requests ,此处唯一需要关注的是 双子电梯线程 的中止条件。
要中止 双子电梯线程 ,需要保证 调度线程 中止且两个电梯共享的 ElevatorRequests 为空且两个电梯内没有人且处于 WAITING 状态。在 ElevatorRequests 内部设置两个 IDLE 变量标记两个电梯是否在闲置中。
在互测阶段出现了一个bug
Request 和 Reset 同时输入时,小几率在输出 RESET_BEGIN 后再输出 RECEIVERESET 后需要立即拿到 ElevatorRequests 的锁RESET其余bug均在本地调试
ElevatorRequestsAB 继承了 ElevatorRequests 但是 isEnd 没有用继承的 isEnd 导致修改仅在父类ElevatorA 转到 ElevatorB 的方法先输出 OUT 再输出 RECEIVEwait() 之前 如果 ElevatorRequests 不为空则设置 IDLE = FALSE 加入请求时 IDLE = TRUEwait() 条件不成立后如果 ElevatorRequests 为空则设置 IDLE = TRUEElevatorRequests 中加入 Request 则设置 IDLE = FALSE在多线程协作环节,需要严格区分两个量: 线程自身运行状态 和 其他线程读取到的该线程状态 。 线程自身运行状态 只有本线程可见,真实反映该线程目前的属性;而为了与其他线程进行写作,该线程需要改变 其他线程读取到的该线程状态 来通知其他线程。通常该线程会对 共享类 进行 sychronized 操作,更新自身对外界的可见状态。
在更新 其他线程读取到的该线程状态 的时候,最重要的是保证更新顺序正确。具体而言,有的时候需要先更新 其他线程读取到的该线程状态 ,再更新 线程自身运行状态 ,但有的时候却是反过来的。这取决于实际应用的场景,比如 双子电梯 在进入 换乘楼层 之前,应当先更新 ifOccupied ,让外界知道 换乘楼层 已经被占用,然后再进入 换乘楼层 。然而在离开 换乘楼层 的时候,应当先离开 换乘楼层 ,然后再更新 ifOccupied 。
此外,还需要识别出哪些操作应当是原子的,加上 sychronized 。由于多线程运行的不确定性,调试时应当使用 print 输入判断程序所在的位置,进而反推线程运行逻辑。更多的时候,要发现和解决这些问题是依赖 静态代码检查 而不是调试。
应该多尝试一些简单的调度策略,替代过于简单的平均分配,如随机分配,调参打分等。要多考虑极限输入和高并发。