301
社区成员
发帖
与我相关
我的任务
分享详细示例可见https://blog.csdn.net/wenqi1992/article/details/125310820
try {
//可能发生异常语句
} catch (InterruptedException e) {
//异常处理语句
}
sleep不会放锁,是Thread的静态本地方法wait后放锁,是Object的静态本地方法wait()不指定等待时长,需要手动notify/notifyAll()唤醒生产者-消费者模式是一种经典的多线程设计模式,用于解决多个线程之间的数据共享和协作问题。在生产者-消费者模式中,有两类线程:生产者线程和消费者线程。它们之间通过共享一个缓冲区(或队列)来协作,生产者将数据放入缓冲区,消费者从缓冲区取出数据并进行处理。
生产者-消费者模式的主要目标是实现生产者和消费者之间的解耦,使它们可以独立地进行工作,从而提高系统的性能和可维护性。

要求:实现电梯调度。给出了请求,指定了请求需要分配给的电梯。
作者在一开始看到作业的时候是不知道如何实现的,毕竟是作者第一次接触多线程设计(当然在三次作业之后作者对多线程有了更深的了解)。于是作者拖到了周三的实验课,在实验课上看到了提供的实验代码,发现其是一个绝妙的架构,故作者三次作业均是基于实验代码。实验万岁!
此架构创建8个线程:inputThread、Schedule、以及六个Elevator。创建waitQueue存储输入的总请求队列,由inputThread和Schedule共享,在Schedule中具体确定请求分配给哪个电梯(第一次作业指定了每一个请求的电梯,故本次Schedule的任务非常简单,甚至不设置都可以完成任务,但是在之后不指定电梯的情况下,Schedule的存在就显得非常有必要了。这也体现了我们架构的可拓展性)。创建processingQueue存储每个电梯分配到的请求队列,由Schedule和对应的Elevator共享。
在此我们可以发现,在多线程中,只有waitQueue和processingQueue(均为RequestQueue类)被多个线程共享,故只需在RequestQueue上加锁,保证线程读写安全。
UML类图

UML协作图

本次任务采用LOOK策略,LOOK策略在往届学长的博客中涉及较多,实现过程较为详细。
我们在电梯线程中调用Strategy类中方法,Strategy类专门负责为电梯运行提供Advice。我们需要注意,电梯的运行策略仅涉及电梯本身状态以及自己的processingQueue;并且,Strategy中所有方法应是只读的,因为它只负责提供策略,不会干涉电梯状态的改变。最终电梯状态的改变由电梯线程自身实现。
伪代码如下:
// 是否需要开门?
if (电梯上有人需要出 || (电梯没有满员 && 电梯外面有人进))
return Advice.OPEN; //开门
// 电梯上是否有人?
if (有人) {
return Advice.MOVE; //移动到下一层
}
// 电梯上没人
else {
// 请求队列不为空
if (!processingQueue.isEmpty()) {
if (电梯移动方向前方有请求) {
return Advice.MOVE; //移动到下一层
}
if (后方有请求 || 该层有请求但是目的地在后方) {
return Advice.REVERSE; //掉头
}
}
// 请求队列为空
if (processingQueue.isEmpty()) {
// 请求队列结束
if (processingQueue.isEnd()) {
return Advice.OVER; //结束线程
}
//请求队列没结束
else {
return Advice.WAIT; //等
}
}
}
由于是第一次多线程作业,本次作业的bug主要出现在有关于多线程的设计上,具体体现在无数的CTLE和RTLE。
线程结束: 我们开了8个并发线程。首先,在程序结束时我们需要保证所有线程结束;其次,在每个线程的任务全部完成时,我们需要保证该线程结束或wait。
waitQueue置end。waitQueue为空,且end。即所有发出请求都被分配,且不会再有新的请求出现。同时将每个processingQueue置end。waitQueue为空,且end,processingQueue为空,且end,电梯上没人。即所有被分配到的请求被完成,且不会再有新的请求出现。processingQueue为空,电梯上没人,但还没有end。即可能还会被分配请求,此时需要wait()。轮询: 轮询问题将陪伴三次作业始终,是造成CTLE的最大罪魁祸首。一般是由于该wait的地方没有wait,出现在线程循环while(true){ /* ... */}中。
要求:
先来讨论不指定分配电梯的问题,这意味着我们需要自己设计调度策略,是各路大佬狂卷性能分的好时机。由于今年新加入的RECEIVE要求,自由竞争这一摆烂到底的方法不可取了。当然,这个时候我们在第一次作业中的Schedule类的设计就可以很好地完成任务,即在Schedule类的分配过程中,添加新方法,以实现自己的调度策略,分配给哪个电梯就让它RECEIVE好了。
一些可行的调度策略:
影子电梯
大佬专属调度策略!!!即在每一次分配请求时,对每一部电梯的目前状态全部进行深克隆,然后模拟每一部电梯完成请求需要的时间,选取时间最短的那个。实现过程中细节很多,难度也很大。这是一种局部最优策略,大部分情况下会得到非常优秀的性能,但是偶尔会出现甚至不如随机分配的性能。由于作者坚持将摆烂精神发扬到底(太菜),没有使用该策略。但是在交流过程中听大佬们的实现细节和过程,也收获良多。
路程最短
一种比较接近正常思路的策略,即在分配请求时,计算每一部电梯到达请求发出楼层的距离,选路程最短的那个。思路简单,代码简洁,也展现出了很优秀的性能。作者本人的思路基本上基于此。
模6大法 or random大法
即记录请求是第几个,第1个请求塞给1号电梯,第2个请求塞给2号电梯....第7个请求塞给1号电梯....。思路非常简洁,代码非常简单。某些少数情况下展现出优越的性能。当然,也可以把它融入高端分配策略中,以实现思路和代码的简化。
然后讨论reset的问题。作者将reset请求也塞进了processingQueue中。注意reset请求有最高优先级。在Strategy中,电梯要获得运行建议,先检查队列里有没有reset请求,有则进行reset操作。
UML类图

UML协作图

调度策略的细节问题: 由于本次作业是自己实现调度策略,过程中采用的骚操作可能比较多,bug首先会出现在不合理的调度中。多测几次就能发现并有针对性的解决。
RTLE: 作者互测被刀了一个RTLE,原因在某一时刻涌入了大量请求,但是只有1部电梯没有在reset状态中,按照作者的调度策略,请求不会分配给正在reset的电梯中,故这些请求全部流入了该部电梯。全程只有这一部电梯在动,由于该部电梯不堪重负,光荣超时。
receiveNum属性,即记录电梯接收到的人数,receive一个就加一,到达了满载人数,该电梯不进行receive操作。那么在上述情况下,该可运行的电梯在该时刻不会接受全部请求,别的请求在别的电梯reset结束后会分配给别的电梯。后续正常实现6部电梯的共同调度。新增双轿厢电梯
本次作业只需要解决双轿厢的问题,如何防止双轿厢的碰撞是问题中的关键。
一些可行的策略:
双轿厢?单轿厢!
来自研讨课大佬分享。把双轿厢仍然认为成单轿厢,保证分配到该双轿厢的请求只由该双轿厢协作完成,同一时刻只能有一个轿厢移动。不需要新增线程,用原线程就能实现。这样可以保证极致的正确性,但是会牺牲部分性能。
新增线程
新增轿厢新开线程,将其看作一个独立的电梯,实行与上次作业相同的分配策略。当某个轿厢移动到换乘楼层时,放下全部乘客后立刻离开。当某个轿厢即将移动到换成楼层,但是上一部电梯还没离开,则循环等待,在每个循环中sleep一段时间,直到检测到换乘楼层不存在电梯为止,正常向换乘楼层移动。
UML类图

UML协作图

debug方法在上述每一次作业的bug分析部分有所涉及。现进行一个小小的总结。
多线程debug是一个令人头疼的问题,调试在此毫无用处,只能使用print大法。但是print本身会影响设备对线程的调度,有的时候加入print后bug无法复现...以作者浅薄的知识和愚笨的大脑,只能总结出以下几点
WA && 不可复现的RTLE
不可复现的RTLE即在本地没问题,但是交上去就是不能通过。一般WA(仅限于线程问题的WA)和RTLE的问题使用print大法行不通,因为print加多了,会影响对线程的调度,使bug无法复现。盲改也不行,改动多了也会影响调度。此时最好借助评测机(大佬开发的面向大众的评测机),一般在大型评测机上能够复现错误,然后使用瞪眼法,找到问题所在地,自己分析问题原因,进行针对性的解决。
可复现的RTLE
可复现也分为线程问题和非线程问题,一般能够自己分辨出。线程问题,线程错误结束、人没送完、人被困电梯等问题,同上,根据错误问题进行针对性解决。非线程问题,如调度策略不够好导致用时过长,如前面提到的同一时刻涌入大量请求,则考虑改进调度策略。
CTLE
主要是轮询。在循环中添加print定位轮询位置,具体内容见Part2 1.3 bug分析
本单元任务是作者第一次接触到多线程处理,对于初学的我来说是一个不小的挑战。理解多线程原理和使用方法让作者感到崩溃,面对无法复现的bug令作者感到更加崩溃。所幸有学长学姐的博客帮助,助教们的工具帮助、debug方法帮助,研讨课大佬的耐心解答和分享,交流群里各位同学实时的、耐心的解答,作者也算是完成了单元任务,并且在一次次尝试中逐渐理解掌握了多线程的原理和使用。
对于线程安全。我们只需要对多个线程共同访问的数据加锁即可。一开始我对请求队列的全部方法加锁(synchronized),因为只有waitQueue和processingQueue(均为RequestQueue类)被多个线程共享。waitQueue由输入线程和调度器线程共享,processingQueue由调度器线程和电梯线程共享。这样的加锁可以解决前两次作业的全部问题。第三次作业因为涉及双轿厢的防碰撞,以及在调度策略中也需要调度器对电梯状态进行频繁地读。即其他电梯线程、调度器线程会对电梯状态进行读,本电梯线程会对电梯状态进行写。这时候使用synchronized对方法加锁不是一个好的选择,因为读操作太频繁了,使用synchronized会损耗很多不必要的时间。因此选择使用读写锁,对读写进行精确的加锁,保证性能和正确性。
对于层次化设计。再次感谢实验代码!输入线程、调度器线程、电梯线程的组合在三次作业中没有进行改变,可以解决所有问题。这也体现出生产者-消费者的思想,同时具有优秀的可拓展性。