面向对象设计与构造 Unit2 博客

曾文轩-22373305 学生 2024-04-20 18:56:49

0 前言

这一单元的OO课程带我们比较系统地学习了一下多线程,给我的感受是给程序世界开辟了一个新的很有趣也很实用的领域。

在之前,想用js实现一些功能,比如说sleep定时完成一些功能的时候,就接触了一些同步和异步的概念。用一些GUI框架如PyQt写一些GUI小东西的时候也使用了一些多线程,包括也思考过比如,按下按钮触发的功能,底层上是通过一个程序while(1)去轮询的吗还是有什么别的机制。通过这个OO单元的学习,让我有了更深的理解。

1 作业整体架构

多线程架构

采用生产者消费者模型

除了Main线程以外,共有Input、Schedule、Buffer、Elevator/HalfElevator四类线程,线程之间通过容器类进行交互

容器类

总共有两类容器类,一类容器类装的对象是Person对象,另一类容器类装的是Reset相关信息。

装Person对象的容器共有:

  • WaitQueue类:装的是新输入的Person和因为Reset导致从电梯的Passenger和ReceivedQueue回到WaitQueue重新分配的Person
  • BufferQueue类:装的是被Schedule调度器分配好了,但是还没有输出Receive的Person
  • ReceivedQueue类:装的是被Buffer线程判断当前Elevator不在Reset状态,进入对应的ReceivedQueue或者双轿厢电梯的ReceivedQueue
  • Passenger类:装的是已进入电梯内的Person

ResetQueue类:存放当前电梯收到的ResetRequest以及此ResetRequest是否已被处理

线程

  • Main线程:创建好所有的容器类,用这些容器类调用构造函数创建好除HalfElevator线程以外的所有线程,实现线程之间的共享对象
  • Input线程:从输入中获取PersonRequest和ResetRequest,把PersonRequest直接交给WaitQueue,ResetRequest交给对应的ResetQueue
  • Schedule线程:从WaitQueue中一个个获得Person,并根据调度策略分配给被分配的电梯的BufferQueue
  • Buffer线程:从BufferQueue中获得Person,完成两件事情
    • 判断当前电梯是否正在Reset,如果正在Reset则等待,如果不在Reset,则完成下一个工作
    • 判断当前电梯是否已经变成双轿厢电梯,如果已经变成双轿厢电梯,则根据Person的from和to分配给对应双轿厢电梯的ReceivedQueue,如果未变成双轿厢电梯,则直接给对应的ReceivedQueue
  • Elevator/HalfElevator线程:从ReceivedQueue获得Person,根据捎带策略决定是否获得并放入Passenger中;从ResetQueue中获得是否有未处理的ResetRequest,进行对应的Reset,并在Reset时把Person从Passenger和ReceivedQueue中全部取出放入WaitQueue中

img

整体架构

除了以上的容器类和线程类以外,还有一个静态类End,存储当前的输入是否结束,以及当前还剩的Request个数,提供一个end()方法,供各个线程判断是否需要结束线程

另外,Main方法创建并start了Input,Schedule,Buffer,Elevator线程,而HalfElevator线程由Elevator在接受了DoubleCarResetRequest,在即将结束处理Reset的时候创建并启动

img

架构在迭代中的变化

第五次作业 到 第六次作业

在第五次作业到第六次作业的迭代中,我的架构基本没有发生变化,代码主要的变化在于,为了Schedule实现影子电梯的调度策略,给Passenger、ReceivedQueue、Elevator类都加了clone方法,并给Elevator类增加了simulate方法,仅供经过clone后,未start成线程的电梯对象调用,来计算得到处理完加上该请求之后总共需要多少时间才能把请求全部处理完。

除此之外,因为此时Schedule线程的终止条件不再是Input结束以及waitQueue为空,而是有可能会因为reset导致已经被分配的请求重新进入waitQueue等待分配,所以需要增加判断所有ReceivedQueue和Passenger是否为空。这种实现判断终止的方法耦合度很高,很麻烦也很丑陋,在下一次修改的过程中,由于架构进一步复杂,变得不可容忍,因而增加了End静态类来实现这个功能。

第六次作业互测bug修复

为了解决“围师必阙”这个bug,代码的架构进行了比较大的调整。为了让正在Reset的电梯也能被分配到PersonRequest,但是不直接输出Receive,而是等到Reset结束之后再输出Receive,我采取了Buffer机制。

为了实现这个机制,我把代码的架构进行了大的改动。

首先,从原来的Input-Schedule-Elevator三层流水线的结构,变成了Input-Schedule-Buffer-Elevator四层流水线的结构,增加了Buffer线程。并且为了实现Schedule和Buffer之间的交互,增加了BufferQueue容器类作为它们之间的共享对象。

由于增加了一层结构,原来本就很复杂的判断终止的方法变得更加麻烦。

因此抛弃了原来的方法,增加了一个End静态类,管理inputEnd这一bool静态变量和personToHandle静态变量,对外提供

  • 被Input线程调用的setInputEnd()静态方法
  • 被Input线程调用的add()静态方法,增加personToHandle
  • 被Elevator线程调用的sub()静态方法,只在Person彻底完成的时候才调用,由于Reset暂时出去不算,Reset的时候,刚好已经完成了,算

这些写方法。

并且提供isEnd()这一读方法,可供所有线程使用。

这样的好处是,不用管在里面传来传去的Person,只抓住新的Person只能从输入中来,只有被彻底解决的时候才算完成了,这一本质,而不用去每次查询各个容器里是否为空。

第六次作业bug修复后版本 到 第七次作业

这次修改主要是增加了HalfElevator线程。

这个线程不在开始的时候启动,但是它用来和Buffer线程交互的ReceivedQueue共享对象在开始的时候就创建好并且给予Buffer线程和Elevator线程。当Elevator需要分裂的时候,才创建并启动HalfElevator线程,并终止Elevator线程。

2 多线程实现——同步块的设置和锁的选择

在第五次和第六次的作业中,我使用的全部都是synchronized同步块。

在同步块的设置上,我基本遵循了在容器类中加synchronized的原则,也就是把容器实现成线程安全的,而不是在线程类中去处理同步问题。

除了为了实现同步的目的而在容器类中设置的同步块以外,还有为了实现wait和notifyAll而设置的对应的同步块。如对于Buffer线程,当电梯在reset时,进入等待状态,而为了防止轮询,在Main类中给每个电梯创建一个Object类的对象resetLock,在判断当前处于reset状态时,进入resetLock为monitor的同步区,进行wait,而当reset处理完成,在Elevator线程中进入resetLock为monitor的同步区进行notifyAll(此处notify也可)。

// Buffer.java
while (elevator.isResetting() && !End.isEnd() && !split) {
    synchronized (resetLock) {
        try {
            resetLock.wait();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }
}

// Elevator.java
synchronized (resetLock) {
    resetLock.notifyAll();
}

而在第七次作业中,我引入了使用起来更加灵活的可重入锁ReentrantLock和可重入读写锁ReentrantReadWriteLock。

在End静态类中,因为判断是否End这个操作读会非常广泛且频繁,而写相对比较少(即设置inputEnd,Person从Input输入和彻底处理完更改当前的数量),所以采用了读写锁。

而为了实现双轿厢电梯的互斥进入,我使用了ReentrantLock,在一个电梯即将输出进入换乘楼层前,把锁lock,而在输出从换乘楼层进入另一个楼层后,把锁unlock。

3 电梯的调度和捎带策略、双轿厢不碰撞的实现

捎带策略

捎带策略在三次作业中统一采用的Look策略,这种策略很自然与合理,效率也很高。

调度策略

在第六次作业中,我采用的是影子电梯策略,对电梯的运行进行模拟,计算出每个电梯运行结束需要的时间,进行分配。但是遇到了围师必阙的bug之后,加了缓冲区之后,使用原影子电梯,如果不进行大规模修改,就又会导致全部分到同一个正在Reset的,比较优的电梯里。所以为了方便直接随机分配了,第七次也直接延续了第六次bug修复后的代码,采用了随机策略。(尽早随机保安宁× 但是不要性能分的后果是性能分真的没了,强测从第六次作业的影子电梯98分变成了第七次作业随机88分)

双轿厢不碰撞的实现

如上所述,我使用了ReentrantLock,在Elevator内处理分裂Reset请求的时候,创建好一个锁,给这两个HalfElevator线程,在每个HalfElevator即将输出进入换乘楼层前,把锁lock,而在输出从换乘楼层进入另一个楼层后,把锁unlock

4 bug分析

在这个单元的作业中,强测未遇到bug,第六次作业的互测中有经典的围师必阙的数据(即同时把5台电梯reset,然后投放大量的PersonRequest,如果没有增加缓冲区,直接让正在Reset的电梯不接受请求,则会将这些请求全部分配给一个电梯,导致需要运行很长的时间)导致的RTLE的bug。

为了解决这个bug,我采取了加入一个缓冲区Buffer线程和BufferQueue容器的方法,让正在Reset的电梯也能接受Person。

5 心得体会

经过多线程的这三次作业,让我对于多线程这个领域算是了入门,也发觉对于很多需要多线程实现的领域比如网页后端和GUI开发里的很多概念有了理解。

同时在这一单元的学习中,我也更加体会到了先确定架构设计再进行代码编写的重要性。

特别是对于多线程的程序,出现了bug再调试的方法更加没那么行得通,所以需要在刚开始就确定好架构设计、线程之间的交互、锁的设置等问题,在进行细致的代码编写。

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

301

社区成员

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

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