BUAA_2024_OO_Unit2_电梯单元总结

谢卓宇-22371061 学生 2024-04-19 23:30:05

「BUAA-OO-2024」第二单元——电梯单元总结

目录

  • 「BUAA-OO-2024」第二单元——电梯单元总结
  • 前言
  • 同步块与锁的设置
  • synchronized块
  • ReentrantLock锁
  • 困惑:哪里该加锁?
  • 架构设计
  • Hw7 UML图解
  • 线程图
  • 区分Request类型最晚原则
  • 电梯分配策略
  • 电梯运行策略
  • 三次作业中稳定和易变的内容
  • 双轿厢电梯的防撞机制——ReentrantLock锁机制
  • 三次作业Bug与处理
  • 轮询
  • 解决方法
  • 线程安全——死锁
  • 心得体会

前言

第二单元以电梯调度系统为主题,多线程编程为核心进行了3次迭代作业。对我而言,本单元的作业的完成难度比第一单元小。但其在debug以及控制线程安全等方面带给我的压力远比第一单元大。这也导致我第二单元的作业完成得非常“憋屈”。也正是因为用去了大把时间进行漏洞,几乎没有时间去完成优化。

同步块与锁的设置

synchronized

synchronized块是这三次作业我所用到最多的同步方法。其优点在于能非常方便的实现语句块的同步。由于能在其中使用notify()等语句,也能够很方便的进行线程的唤醒

ReentrantLock

在hw7的双轿厢电梯防撞设计中,我使用了该种锁。ReentrantLock在这里有着显著的优势。具体的实现会在后文防撞设计介绍中加以阐述

困惑:哪里该加锁?

3次作业中,我始终有这样的困惑——到底哪些地方应该为其添加同步块(或者锁)。一开始我认为任何涉及到共享资源读写的部分都应该加上锁。但因此出现了非常多死锁bug且难以在本地复现。在多次实验以及体会中,我总结出了一些加锁的tips:

  • 不是所有涉及共享对象读写的地方都需要加锁
  • 谨慎或尽量不要进行多层加锁
  • 首要选择在共享对象类的方法上加锁。而一旦该方法加锁,就不要再在调用该方法前再加锁。
  • 对于共享资源并非我们所定义的特殊类——例如电梯的一些状态(这些状态可能使用布尔型变量、int变量、HashMap或是ArrayList等等),对于这些状态的读写(例如在电梯线程本身以及调度器Schedule线程会涉及共同读写),往往不必全部加锁。在难以判别是否该加锁时,不妨尝试短暂sleep。能有效解决线程读写的安全(因为对这类变量的读和写速度非常非常快)

架构设计

Hw7 UML图解

为什么直接分析hw7的UML图而不是将三次hw的架构全部进行分析。是因为我的代码在hw7进行了完全重构。重构后的代码不仅解决了hw6的线程安全问题,也为hw7的迭代提供了非常舒适方便的条件。因此,我们直接看最佳的架构。

img

架构的基本核心依然是课上推荐的结构。采用生产者—消费者模型。生产者即所定义的RequestTable类,消费者即所有会访问到这个类的线程。这里就不再赘述。下面重点对架构中的一些特殊点进行分析

线程图

img

区分Request类型最晚原则

和在hw5与hw6中不同,hw7中我不再在InputHandler中就提前细分好所读到的请求究竟是乘客请求,还是重置请求。不管是何种请求,我们都将其加入到一个统一的最大请求队列当中。等待后续处理。

这里要注意的是,虽然我们将其都加入到同一个请求队列中,但在InputHandler中还是需要进行一定的区分处理。具体是:如果请求为重置请求,我们将其添加到队列最前面。如果请求为乘客请求,正常添加到最后即可。这样做的目的是:保证调度器会优先调度重置请求,让电梯最先接到并处理重置请求,防止出现accept到重置请求后,电梯运行层数超出2层限制的情况。

具体区分是什么请求的时候事实上是在调度器和电梯线程中。调度器对于请求的分配与InputHandler处理几乎一致——即对应电梯的重置请求应放在电梯的等待队列最前。电梯线程则只需判断其等待队列的队首是何种请求即可进行对应操作。

该原则的优点在于。在后续的处理中,我们经常会出现电梯正在休眠,但当有重置请求或者乘客请求时需要将其唤醒的情况。不区分请求类型而是将所有请求进行同样分配与处理,能够减少特判,方便唤醒线程。同时简化了代码逻辑。

电梯分配策略

很惭愧的是,由于对影子电梯等先进分配方法的理解不深入以及惧怕心理,同时自身时间问题。我在hw6与hw7中都采用了最简单的均分(即模6策略)。根据两次强测的情况,模6策略的整体性能显然是远低于影子电梯、调参等策略的。但其实现较为容易,且更易debug(这是重点),整体性能实际上能满足现实需求。因此个人认为模6策略是在无法完成更优分配策略的前提下,最能够实现的。

电梯运行策略

电梯运行策略这里没什么好说的,大家采用的基本上都是传统的Look策略,性能方面也是很均衡的。

三次作业中稳定和易变的内容

在hw7重构后,3次作业在其中分别拓展和稳定的部分结构是很清晰的

  • 无论是最初的电梯还是双轿厢电梯,Strategy策略都未曾变过
  • 请求的分配方法和处理是稳定的
  • 双轿厢电梯为了实现两个轿厢在各自能自由移动的情况下并不相撞这一目的,和普通电梯存在明显的区别。因此我新建一类——DcElevator类。用于专门代表双轿厢电梯。这里是最大的拓展部分。

双轿厢电梯的防撞机制——ReentrantLock锁机制

在hw7中,新增了双轿厢电梯。该电梯有2个轿厢A、B以及一个确定的换乘楼层L。规定A与B不能同时到达L,即必须在其中一个轿厢离开换乘楼层后,另一个轿厢才可以进入换乘楼层。

需要注意的是,两个轿厢我们应视为独立的两部电梯。不能因为图方便与安全,每一次只允许其中一个轿厢移动。这两部电梯间的唯一联系应是不能同时在换乘楼层即可。

分析需求可知,我们需要建立一种互斥锁的机制,只有拿到锁的线程(电梯)才有资格进入换乘楼层,而想进入换乘楼层却还没有拿到锁的线程则应阻塞。直到拿到锁才能执行。而ReentrantLock锁天然地满足这个需求。

具体的实现流程是,我们构造一个ReentrantLocklock,

随后,将这个lock作为两个电梯线程的构造参数,这样便能实现两个线程共用一把锁,

接下来,我们需要定义一个需要拿到锁才能执行的行为,在这里就指的是前往换乘楼层。这个行为应满足在执行后便能够释放锁。

下面是伪代码:

ReentrantLock lock = new ReentrantLock();

DcElevator newDC1 = new DcElevator(..., lock, ...);
DcElevator newDC2 = new DcElevator(..., lock, ...);

private void goToTransferFloor() {
    lock.lock();
    try {
        dosomething;
    } finally {
        lock.unlock();
    }
}

而拿到锁进入换乘楼层的线程什么时候释放锁呢?我们说应该在其离开换乘楼层后方可释放锁。

很显然这个行为应该是:前往换乘楼层——放人与进人——离开换乘楼层

这个行为决定了一个电梯不能够在换乘楼层久留。这也很容易理解,电梯如果长时间在换乘楼层是不安全的。

这样,便能够实现防撞了。。

三次作业Bug与处理

三次作业出现的Bug太多了,,,层出不穷。。。

轮询

这是导致我hw5爆炸的原因。轮询在中测中没有测出,在公共测评机中也没有测出,强测中多个点因为轮询造成CPU超时。

解决方法

轮询出现在线程的Run当中,利用打印的方法能较为容易找出轮询的具体位置。首先在while循环开始进行打印检查。如果出现大量打印结果(几万几十万这种),那么说明这个线程出现了轮询。再将打印语句进一步放在while的其他分支里,缩小范围即能找到。

线程安全——死锁

这是我本单元的噩梦。。因为在评测机上随机出现,而本地几乎复现不了导致我像一直在对空气debug。

我在这里提出几个我用到的解决思路:

  • 谨慎加锁(前文已有阐述)
  • 少用或避免使用多个容器储存电梯中的状态并供给给其他线程使用,而选择直接将电梯线程给到其他线程。
  • 重构重构重构。。。

心得体会

电梯单元就这样草草地结束了。回顾过去1个月,对我来说的确非常煎熬。一是多线程编程的不熟悉导致出现很多线程安全的bug而且难以解决。其次是对于优化的调度算法实现心有余而力不足,最终也只用了均分策略解决大部分问题。但在这三次作业的层层磨练中,我也掌握了多线程编程的基本要求、基本思路,以及保证线程安全的处理。老师在课上讲过,未来工作中多线程编程将是我们面对的最主要的模式之一。因此,未来我也要多加巩固与提升。在多线程编程方面继续学习。

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

301

社区成员

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

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