2024 OO Unit2

陆辰-22371569 学生 2024-04-17 21:24:13

2024 OO Unit2


OO Unit2主题为模拟多线程实时电梯系统,涉及线程协作、同步与锁的使用、调度架构设计等内容

三次作业架构上是逐级迭代的,基本上第一次架构决定了整个单元的框架,所以直接从HW5开始讲起,后面HW6、HW7逐渐新增内容。

HW5

UML与线程协作图 HW5

img

img

整体上参考了指导书和往年学长博客,采用了 输入线程-调度线程-电梯线程 结构,线程之间采用 共享类 进行同步, 对 共享类 的访问均采用 sychronized 块,保证块内操作的原子性与可见性,同时在 sychronized 块内均使用 notifyAll() 方法防止死锁。这是基本稳定的架构内容,变化的内容会在每个版块中介绍。

线程启动 HW5

在程序开始时,首先由 主线程 启动 输入线程调度线程 ,而 调度线程 在被启动后,启动六个 电梯线程

输入线程工作 HW5

对于 输入线程 ,其任务为接收并包装主线程的输入(官方包已经提供),提取出 Request 类交给 调度线程 处理。两个线程之间通过共享 WaitingRequests 类进行协作,采取 生产者-消费者 模式, 输入线程 生产请求 RequestWaitingRequests ,而 调度线程 消费 WaitingRequests 中的 Request

调度线程工作 HW5

对于 调度线程 ,其任务为决定哪个 电梯线程 应该处理哪一个 Request 。如上所述,作为 WaitingRequests 的消费者, 调度线程 从中获取需要进行分配的 Request

为了防止 WaitingRequests 为空时 调度线程 轮询浪费CPU时间,当 WaitingRequests 为空且 输入线程 未中止时, 调度线程 会调用 WaitingRequests.wait() 方法释放锁并休眠,直到被 输入线程 调用 WaitingRequests.notifyAll() 方法唤醒,检查 WaitingRequests 是否仍为空。如果仍未空则重新获取 Request ,不为空则进行调度。

同时, 调度线程电梯线程 之间通过 ElevatorRequests 进行共享, 每个 电梯线程 都有自己对应的 ElevatorRequests ,而 调度线程 可以访问这六个 ElevatorRequests 中的任意一个。 调度线程电梯线程 仍采用 生产者-消费者 模式,但生产者是 调度线程 ,消费者是 电梯线程 。当 调度线程 向某一个 电梯线程ElevatorRequests 生产了一个 Request ,说明该 Request调度线程 分配给了该 电梯线程

分配策略 HW5

调度线程分配策略 上,我选择了指导书提供的算法。基本逻辑是,第一个 Request 分配给第一个 电梯线程 ,第二个 Request 分配给第二个 电梯线程 ,直到第六个 Request 分配给第六个 电梯线程 。然后将第七个 Request 分配给第一个 电梯线程 ,第八个 Request 分配给第一个 电梯线程 ,以此类推。

这种分配方法的优点在于实现简单,各个 电梯线程 之间负载均匀,不容易出现一个 电梯线程 承载所有 Request 的情况。缺点是无法根据 电梯线程 当前的运行状况分配 Request ,利用电梯可以捎带同向 Request 节约时间的特性。

电梯线程工作 HW5

对于 电梯线程 ,其任务为根据其对应的 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 用于决策电梯下一步应该进行什么操作。

电梯线程 实际运行中采取了 状态机 写法。

img


电梯有三种状态 MOVING-WAITING-WORKING ,有六种动作 UP-DOWN-START-WAIT-OPEN-CLOSE 。内置的 ElevatorStrategy 会根据 ElevatorInfo 判断下一步应采取什么动作。

电梯运行策略 HW5

第一种方法为指导书提供的 ALS ,新增 主请求被捎带请求 两个概念,优先处理 主请求 ,顺路捎带 其他请求

  • $1、$主请求选择规则:

    • $(1)、$如果电梯中没有乘客,将请求队列中到达时间最早的请求作为主请求
    • $(2)、$如果电梯中有乘客,将其中到达时间最早的乘客请求作为主请求
  • $2、$被捎带请求选择规则:

    • $(1)、$电梯的主请求存在
    • $(2)、$该请求投喂的时刻小于等于电梯到达该请求出发楼层关门的截止时间
    • $(3)、$电梯的运行方向和该请求的目标方向一致

第二种方法为往届博客和现实中电梯都采用的 LOOK ,新增一个 currentDir 属性

  • $1、$电梯只会沿着 currentDir 方向移动
  • $2、$只会捎带同向的请求
  • $3、$currentDir 仅在以下情况发生时转向
    • $(1)、$触顶/触底
    • $(2)、$电梯空 & 没有可捎带请求 & currentDir 方向前面的楼层没有人

在实现了 ALS 之后,我听说 LOOK 更现实,就尝试了 LOOK ,并且基本上以后都沿用了 LOOK

线程中止 HW5

输入线程 ,当读到 NULL 就可以中止。中止之前,在 WaitingRequests 中设置 isEnd=true

调度线程 ,当 WaitingRequests 为空且内部 isEnd=true 时就可以中止。中止之前,在各个 ElevatorRequests 中设置 isEnd=true

电梯线程 ,当 ElevatorRequests 为空且内部 isEnd=true 且电梯内无人且电梯处于 WAITING 是就可以中止。

bug分析 HW5

在互测阶段没有出现bug,以下bug在中测和本地出现

  • $1、$报错current thread is not the owner
    • $(1)、$考虑 sychronized 的对象
    • $(2)、$可能是当前类 即this 或 sychronzied 方法
    • $(3)、$也可能是引用的 共享类
  • $2、$电梯在 STARTWAIT 状态之间循环
    • $(1)、$ ElevatorStrategy 内在 WAITING 时判断是否 OPEN
    • $(2)、$既要考虑上客,也要考虑下客
  • $(3)、$本地可以正常上下客 用datainput+cmd运行最后不关门
    • $(1)、$中止电梯线程时状态必须是 WAITING
    • $(2)、$否则会导致中止时门还是开着
  • $(4)、$OPEN-CLOSE 循环 ElevatorStrategy 发现要进人-> OPEN -> 没进人 -> CLOSE
    • $(1)、$因为 ElevatorStrategy 里只要 电梯不满 & 该层上梯 就开门 但是 上客 过程中只捎带 同向
    • $(2)、$ ElevatorStrategy 应该比 上客 条件更严格/一致
  • $(5)、$电梯内还有人 还有请求未处理 MOVING-WAITING 循环
    • $(1)、$捎带的请求中途到达终点 没有开门放下
    • $(2)、$应该是为了捎带停下 但是开门失败
    • $(3)、$在 ElevatorStrategy 中分析 MOVING 时首先考虑是否有人到达目标楼层 应返回 OPEN
  • $(6)、$电梯不开门接人
    • $(1)、$ ElevatorStrategyMOVING 里遗漏了开门捎带同向
  • $(7)、$电梯要先上到顶楼再下来接向下的请求
    • $(1)、$ ElevatorStrategyMOVING 里遗漏了开门转向:电梯空 & 没有可捎带请求 & currentDir 方向前面的楼层没有人
  • $(8)、$电梯在没有请求的时候会 OPEN-CLOSE 循环
    • $(1)、$ ElevatorStrategyWAITING 判断 OPEN 时需要保证该层有反向请求

使用状态机让电梯的动作和逻辑分的很清晰,修改起来很方便, 但是也将电梯的判断逻辑分散了,容易因为不一致出现死循环。

HW6

UML与线程协作图 HW6

img

img

HW6新增了 RESET 请求和 RECEIVE 输出。

RESET 请求需要尽快处理,处理前电梯里的人需要赶出电梯重新 RECEIVE ,处理时 调度线程 不能向该电梯调度。

RECEIVE 需要在 调度线程 调度 Request电梯线程 时输出。一个乘客只能分配给一个电梯。RESET 可以使得 RECEIVE 中途取消。 电梯必须先收到 RECEIVE 再开始移动。

输入线程工作 HW6新增

因为 RESET 请求需要尽快处理,我担心传给 调度线程 再传给 电梯线程 会赶不上,就决定先在 WaitingRequests 中标记该 电梯线程 正在 RESET 中,然后在 ElevatorRequests 中传递 RESET 请求。

调度线程工作 HW6新增

在HW6 调度线程 不能向正在 RESET 的电梯调度,所以在调度之前需要根据 WaitingRequests 中的标记判断该电梯是否在 RESET ,如果在 RESET 则需要换一个电梯调度。

电梯线程工作 HW6新增

由于在HW6中 电梯线程RESET 之前需要出人,所以需要向 WaitingRequests 中生产 Request 。在重置结束后,还需要向 WaitingRequests 中标记该电梯重置结束。

线程中止 HW6新增

调度线程 ,判断中止的时候还需要判断是否有电梯在重置。

bug分析 HW6

在互测阶段出现了一个bug

  • $1、$当5个电梯在 RESET 时高并发会RTLE
    • $(1)、$因为 调度线程 将所有 Request 都调度给了剩下的唯一一个电梯。
    • $(2)、$所以当 调度线程 发现一个电梯在 RESET 时,让 调度线程 调用 sleep() 休眠一段时间。

其余bug均在本地调试

  • $2、$ OPENCLOSE 同时输出
    • $(1)、$ RESET 时赶人出门也需要间隔一段时间
    • $(2)、$需要调用 sleep() 方法
  • $3、$电梯尚未输出 RESET_BEGIN 时,已经有另一电梯 RECEIVE 该电梯内乘客
    • $(1)、$需要保证先输出 RESET_BEGINRECEIVE 被赶出的乘客
    • $(2)、$首先将电梯内的人送回 ElevatorRequests
    • $(3)、$输出 RESET_BEGIN
    • $(4)、$再将 ElevatorRequests 的所有人送回 WaitingRequests
  • $4、$重写 LOOKStrategyWAITINGWORKING 时考虑转向
    • $(1)、$电梯内有向下的人未送完 中途有人下去后却转向
    • $(2)、$ WORKING 要电梯无人才能考虑转向
  • $5、$在 WaitingRequest 内记录电梯是否在 RESET 并更改 Scheduler 中止条件
  • $6、$在 Scheduler 调度时应先输出 RECEIVE 在进行调度操作

HW7

UML与线程协作图 HW7

img

img

新增了 双子电梯 ,在同一电梯井内运行,指定一个 换乘楼层 ,两个电梯只能在 换乘楼层 之上或之下运行,且不会接到重置请求。

线程启动 HW7新增

当受到 双重重置 请求时, 电梯线程 会创建两个特殊的 双子电梯线程 ,这两个电梯仍共享同一个 ElevatorRequests ,仍然在 WaitingRequests 中被认为是一个 电梯线程

输入线程工作 HW7新增

接收到 双重重置 请求时,先在 WaitingRequests 中标记该 电梯线程 正在 RESET 中,然后在 ElevatorRequests 中传递 RESET 请求。

调度线程工作 HW7新增

调度线程 接收到 双重重置 请求时需要创建出对应的两个 双子电梯线程 ,替代原来的 电梯线程 。在进行调度时,仍然当做是一个电梯进行调度。

双子电梯线程工作 HW7新增

对于 双子电梯线程 ,由于两个电梯的运行楼层有限,故在接收请求的时候,在 ElevatorInfo 中储存被分配到的请求 RealRequests ,而电梯运行则依照经过处理的请求 DealtRequests 。处理过的请求会被限制在当前电梯可运行的楼层范围内,并在 IN-OUT 时判断该 Request 是真正到达了目标楼层,还是仅仅到达了 换乘楼层 。如果请求还没有到达目标楼层,则会交给另一个 双子电梯线程 处理。

为了避免相撞,在 ElevatorRequests 储存一个变量 ifOccpuied ,表示是否有电梯在 换乘楼层 。当有电梯要进入 换乘楼层 ,会先检查该变量 ifOccpuied ,如果结果为 true 则等待,直到变成 false ,将其置为 true 再进入该楼层。当有电梯要离开 换乘楼层 时,先离开换乘楼层,再将其置为 false

线程中止 HW7新增

由于 双子电梯线程 不会向 WaitingRequests 生产 Requests ,此处唯一需要关注的是 双子电梯线程 的中止条件。

要中止 双子电梯线程 ,需要保证 调度线程 中止且两个电梯共享的 ElevatorRequests 为空且两个电梯内没有人且处于 WAITING 状态。在 ElevatorRequests 内部设置两个 IDLE 变量标记两个电梯是否在闲置中。

bug分析 HW7

在互测阶段出现了一个bug

  • $1、$当 RequestReset 同时输入时,小几率在输出 RESET_BEGIN 后再输出 RECEIVE
    • $(1)、$调度前判断不在 RESET 后需要立即拿到 ElevatorRequests 的锁
    • $(2)、$否则该电梯可能会被 RESET

其余bug均在本地调试

  • $2、$ElevatorRequestsAB 继承了 ElevatorRequests 但是 isEnd 没有用继承的 isEnd 导致修改仅在父类
  • $3、$从 ElevatorA 转到 ElevatorB 的方法先输出 OUT 再输出 RECEIVE
    • $(1)、$重置双子电梯时,运行速度也要改
  • $4、$在电梯 wait() 之前 如果 ElevatorRequests 不为空则设置 IDLE = FALSE 加入请求时 IDLE = TRUE
  • $5、$wait() 条件不成立后如果 ElevatorRequests 为空则设置 IDLE = TRUE
  • $6、$如果向 ElevatorRequests 中加入 Request 则设置 IDLE = FALSE

心得体会

在多线程协作环节,需要严格区分两个量: 线程自身运行状态其他线程读取到的该线程状态线程自身运行状态 只有本线程可见,真实反映该线程目前的属性;而为了与其他线程进行写作,该线程需要改变 其他线程读取到的该线程状态 来通知其他线程。通常该线程会对 共享类 进行 sychronized 操作,更新自身对外界的可见状态。

在更新 其他线程读取到的该线程状态 的时候,最重要的是保证更新顺序正确。具体而言,有的时候需要先更新 其他线程读取到的该线程状态 ,再更新 线程自身运行状态 ,但有的时候却是反过来的。这取决于实际应用的场景,比如 双子电梯 在进入 换乘楼层 之前,应当先更新 ifOccupied ,让外界知道 换乘楼层 已经被占用,然后再进入 换乘楼层 。然而在离开 换乘楼层 的时候,应当先离开 换乘楼层 ,然后再更新 ifOccupied

此外,还需要识别出哪些操作应当是原子的,加上 sychronized 。由于多线程运行的不确定性,调试时应当使用 print 输入判断程序所在的位置,进而反推线程运行逻辑。更多的时候,要发现和解决这些问题是依赖 静态代码检查 而不是调试。

应该多尝试一些简单的调度策略,替代过于简单的平均分配,如随机分配,调参打分等。要多考虑极限输入和高并发。


...全文
61 回复 打赏 收藏 转发到动态 举报
写回复
用AI写文章
回复
切换为时间正序
请发表友善的回复…
发表回复

301

社区成员

发帖
与我相关
我的任务
社区描述
2023年北航面向对象设计与构造
学习 高校
社区管理员
  • YannaZhang
  • CajZella
  • C_ecelia
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告
暂无公告

试试用AI创作助手写篇文章吧