301
社区成员
发帖
与我相关
我的任务
分享这一单元的OO课程带我们比较系统地学习了一下多线程,给我的感受是给程序世界开辟了一个新的很有趣也很实用的领域。
在之前,想用js实现一些功能,比如说sleep定时完成一些功能的时候,就接触了一些同步和异步的概念。用一些GUI框架如PyQt写一些GUI小东西的时候也使用了一些多线程,包括也思考过比如,按下按钮触发的功能,底层上是通过一个程序while(1)去轮询的吗还是有什么别的机制。通过这个OO单元的学习,让我有了更深的理解。
采用生产者消费者模型
除了Main线程以外,共有Input、Schedule、Buffer、Elevator/HalfElevator四类线程,线程之间通过容器类进行交互
总共有两类容器类,一类容器类装的对象是Person对象,另一类容器类装的是Reset相关信息。
装Person对象的容器共有:
ResetQueue类:存放当前电梯收到的ResetRequest以及此ResetRequest是否已被处理

除了以上的容器类和线程类以外,还有一个静态类End,存储当前的输入是否结束,以及当前还剩的Request个数,提供一个end()方法,供各个线程判断是否需要结束线程
另外,Main方法创建并start了Input,Schedule,Buffer,Elevator线程,而HalfElevator线程由Elevator在接受了DoubleCarResetRequest,在即将结束处理Reset的时候创建并启动

在第五次作业到第六次作业的迭代中,我的架构基本没有发生变化,代码主要的变化在于,为了Schedule实现影子电梯的调度策略,给Passenger、ReceivedQueue、Elevator类都加了clone方法,并给Elevator类增加了simulate方法,仅供经过clone后,未start成线程的电梯对象调用,来计算得到处理完加上该请求之后总共需要多少时间才能把请求全部处理完。
除此之外,因为此时Schedule线程的终止条件不再是Input结束以及waitQueue为空,而是有可能会因为reset导致已经被分配的请求重新进入waitQueue等待分配,所以需要增加判断所有ReceivedQueue和Passenger是否为空。这种实现判断终止的方法耦合度很高,很麻烦也很丑陋,在下一次修改的过程中,由于架构进一步复杂,变得不可容忍,因而增加了End静态类来实现这个功能。
为了解决“围师必阙”这个bug,代码的架构进行了比较大的调整。为了让正在Reset的电梯也能被分配到PersonRequest,但是不直接输出Receive,而是等到Reset结束之后再输出Receive,我采取了Buffer机制。
为了实现这个机制,我把代码的架构进行了大的改动。
首先,从原来的Input-Schedule-Elevator三层流水线的结构,变成了Input-Schedule-Buffer-Elevator四层流水线的结构,增加了Buffer线程。并且为了实现Schedule和Buffer之间的交互,增加了BufferQueue容器类作为它们之间的共享对象。
由于增加了一层结构,原来本就很复杂的判断终止的方法变得更加麻烦。
因此抛弃了原来的方法,增加了一个End静态类,管理inputEnd这一bool静态变量和personToHandle静态变量,对外提供
这些写方法。
并且提供isEnd()这一读方法,可供所有线程使用。
这样的好处是,不用管在里面传来传去的Person,只抓住新的Person只能从输入中来,只有被彻底解决的时候才算完成了,这一本质,而不用去每次查询各个容器里是否为空。
这次修改主要是增加了HalfElevator线程。
这个线程不在开始的时候启动,但是它用来和Buffer线程交互的ReceivedQueue共享对象在开始的时候就创建好并且给予Buffer线程和Elevator线程。当Elevator需要分裂的时候,才创建并启动HalfElevator线程,并终止Elevator线程。
在第五次和第六次的作业中,我使用的全部都是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。
捎带策略在三次作业中统一采用的Look策略,这种策略很自然与合理,效率也很高。
在第六次作业中,我采用的是影子电梯策略,对电梯的运行进行模拟,计算出每个电梯运行结束需要的时间,进行分配。但是遇到了围师必阙的bug之后,加了缓冲区之后,使用原影子电梯,如果不进行大规模修改,就又会导致全部分到同一个正在Reset的,比较优的电梯里。所以为了方便直接随机分配了,第七次也直接延续了第六次bug修复后的代码,采用了随机策略。(尽早随机保安宁× 但是不要性能分的后果是性能分真的没了,强测从第六次作业的影子电梯98分变成了第七次作业随机88分)
如上所述,我使用了ReentrantLock,在Elevator内处理分裂Reset请求的时候,创建好一个锁,给这两个HalfElevator线程,在每个HalfElevator即将输出进入换乘楼层前,把锁lock,而在输出从换乘楼层进入另一个楼层后,把锁unlock
在这个单元的作业中,强测未遇到bug,第六次作业的互测中有经典的围师必阙的数据(即同时把5台电梯reset,然后投放大量的PersonRequest,如果没有增加缓冲区,直接让正在Reset的电梯不接受请求,则会将这些请求全部分配给一个电梯,导致需要运行很长的时间)导致的RTLE的bug。
为了解决这个bug,我采取了加入一个缓冲区Buffer线程和BufferQueue容器的方法,让正在Reset的电梯也能接受Person。
经过多线程的这三次作业,让我对于多线程这个领域算是了入门,也发觉对于很多需要多线程实现的领域比如网页后端和GUI开发里的很多概念有了理解。
同时在这一单元的学习中,我也更加体会到了先确定架构设计再进行代码编写的重要性。
特别是对于多线程的程序,出现了bug再调试的方法更加没那么行得通,所以需要在刚开始就确定好架构设计、线程之间的交互、锁的设置等问题,在进行细致的代码编写。