BUAA-OO-Unit2 单元总结

meteoriterr 学生 2024-04-19 22:06:27

BUAA OO UNIT2 单元总结

Part0 新知识梳理

0.1 syncronized一些用法

详细示例可见https://blog.csdn.net/wenqi1992/article/details/125310820

0.1.1 异常处理语句

try {
    //可能发生异常语句
} catch (InterruptedException e) {
    //异常处理语句
}

0.1.2 sleep & wait & notify/notifyAll

  • 线程调sleep不会放锁,是Thread的静态本地方法
  • 线程调wait后放锁,是Object的静态本地方法
  • 如果调用wait()不指定等待时长,需要手动notify/notifyAll()唤醒

    0.1.3 join

    join()执行后线程进入阻塞状态,例如线程A中调用线程B的join(),那么线程A会进入到阻塞队列,直到线程B结束

0.2 生产者消费者模式

生产者-消费者模式是一种经典的多线程设计模式,用于解决多个线程之间的数据共享和协作问题。在生产者-消费者模式中,有两类线程:生产者线程和消费者线程。它们之间通过共享一个缓冲区(或队列)来协作,生产者将数据放入缓冲区,消费者从缓冲区取出数据并进行处理。

生产者-消费者模式的主要目标是实现生产者和消费者之间的解耦,使它们可以独立地进行工作,从而提高系统的性能和可维护性。

0.3 观察者模式

img

Part1 第一次作业

要求:实现电梯调度。给出了请求,指定了请求需要分配给的电梯。

1.1 架构设计

作者在一开始看到作业的时候是不知道如何实现的,毕竟是作者第一次接触多线程设计(当然在三次作业之后作者对多线程有了更深的了解)。于是作者拖到了周三的实验课,在实验课上看到了提供的实验代码,发现其是一个绝妙的架构,故作者三次作业均是基于实验代码。实验万岁!

此架构创建8个线程:inputThreadSchedule、以及六个Elevator。创建waitQueue存储输入的总请求队列,由inputThreadSchedule共享,在Schedule中具体确定请求分配给哪个电梯(第一次作业指定了每一个请求的电梯,故本次Schedule的任务非常简单,甚至不设置都可以完成任务,但是在之后不指定电梯的情况下,Schedule的存在就显得非常有必要了。这也体现了我们架构的可拓展性)。创建processingQueue存储每个电梯分配到的请求队列,由Schedule和对应的Elevator共享。

在此我们可以发现,在多线程中,只有waitQueueprocessingQueue(均为RequestQueue类)被多个线程共享,故只需在RequestQueue上加锁,保证线程读写安全。

UML类图

img

UML协作图

img

1.2 调度策略

本次任务采用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; //等
        }   
    }
}

1.3 bug分析

由于是第一次多线程作业,本次作业的bug主要出现在有关于多线程的设计上,具体体现在无数的CTLE和RTLE。

  • 线程结束: 我们开了8个并发线程。首先,在程序结束时我们需要保证所有线程结束;其次,在每个线程的任务全部完成时,我们需要保证该线程结束或wait。

    • inputThread 结束条件:输入结束则return,同时将waitQueue置end。
    • Schedule 结束条件:waitQueue为空,且end。即所有发出请求都被分配,且不会再有新的请求出现。同时将每个processingQueue置end。
    • Elevator:
      • 结束条件:waitQueue为空,且end,processingQueue为空,且end,电梯上没人。即所有被分配到的请求被完成,且不会再有新的请求出现。
      • 等待条件:processingQueue为空,电梯上没人,但还没有end。即可能还会被分配请求,此时需要wait()。
  • 轮询: 轮询问题将陪伴三次作业始终,是造成CTLE的最大罪魁祸首。一般是由于该wait的地方没有wait,出现在线程循环while(true){ /* ... */}中。

    • 定位:方法来自于慷慨的助教,即在while中设置特殊字符串输出,如“********”,若发现某个while中输出了很多很多(几万甚至几十万)行特殊字符串,则可以确定是该循环处出现轮询。接下来可以在该循环内部的循环或者if中重复上述方法,直到定位到最小单元,就找到了轮询发生的位置啦!

Part2 第二次作业

要求:

  1. 不指定分配电梯
  2. 增加reset指令,可以改变电梯的运行状态

2.1 架构设计

先来讨论不指定分配电梯的问题,这意味着我们需要自己设计调度策略,是各路大佬狂卷性能分的好时机。由于今年新加入的RECEIVE要求,自由竞争这一摆烂到底的方法不可取了。当然,这个时候我们在第一次作业中的Schedule类的设计就可以很好地完成任务,即在Schedule类的分配过程中,添加新方法,以实现自己的调度策略,分配给哪个电梯就让它RECEIVE好了。

一些可行的调度策略:

  • 影子电梯
    大佬专属调度策略!!!即在每一次分配请求时,对每一部电梯的目前状态全部进行深克隆,然后模拟每一部电梯完成请求需要的时间,选取时间最短的那个。实现过程中细节很多,难度也很大。这是一种局部最优策略,大部分情况下会得到非常优秀的性能,但是偶尔会出现甚至不如随机分配的性能。由于作者坚持将摆烂精神发扬到底(太菜),没有使用该策略。但是在交流过程中听大佬们的实现细节和过程,也收获良多。

  • 路程最短
    一种比较接近正常思路的策略,即在分配请求时,计算每一部电梯到达请求发出楼层的距离,选路程最短的那个。思路简单,代码简洁,也展现出了很优秀的性能。作者本人的思路基本上基于此。

  • 模6大法 or random大法
    即记录请求是第几个,第1个请求塞给1号电梯,第2个请求塞给2号电梯....第7个请求塞给1号电梯....。思路非常简洁,代码非常简单。某些少数情况下展现出优越的性能。当然,也可以把它融入高端分配策略中,以实现思路和代码的简化。

然后讨论reset的问题。作者将reset请求也塞进了processingQueue中。注意reset请求有最高优先级。在Strategy中,电梯要获得运行建议,先检查队列里有没有reset请求,有则进行reset操作。

UML类图

img

UML协作图

img

2.2 bug分析

  • 调度策略的细节问题: 由于本次作业是自己实现调度策略,过程中采用的骚操作可能比较多,bug首先会出现在不合理的调度中。多测几次就能发现并有针对性的解决。

  • RTLE: 作者互测被刀了一个RTLE,原因在某一时刻涌入了大量请求,但是只有1部电梯没有在reset状态中,按照作者的调度策略,请求不会分配给正在reset的电梯中,故这些请求全部流入了该部电梯。全程只有这一部电梯在动,由于该部电梯不堪重负,光荣超时。

    • 解决办法:作者在电梯类中新增receiveNum属性,即记录电梯接收到的人数,receive一个就加一,到达了满载人数,该电梯不进行receive操作。那么在上述情况下,该可运行的电梯在该时刻不会接受全部请求,别的请求在别的电梯reset结束后会分配给别的电梯。后续正常实现6部电梯的共同调度。

Part3 第三次作业

新增双轿厢电梯

3.1 架构设计

本次作业只需要解决双轿厢的问题,如何防止双轿厢的碰撞是问题中的关键。

一些可行的策略:

  • 双轿厢?单轿厢!
    来自研讨课大佬分享。把双轿厢仍然认为成单轿厢,保证分配到该双轿厢的请求只由该双轿厢协作完成,同一时刻只能有一个轿厢移动。不需要新增线程,用原线程就能实现。这样可以保证极致的正确性,但是会牺牲部分性能。

  • 新增线程
    新增轿厢新开线程,将其看作一个独立的电梯,实行与上次作业相同的分配策略。当某个轿厢移动到换乘楼层时,放下全部乘客后立刻离开。当某个轿厢即将移动到换成楼层,但是上一部电梯还没离开,则循环等待,在每个循环中sleep一段时间,直到检测到换乘楼层不存在电梯为止,正常向换乘楼层移动。

UML类图

img

UML协作图

img

Part4 面对多线程的debug方法

debug方法在上述每一次作业的bug分析部分有所涉及。现进行一个小小的总结。

多线程debug是一个令人头疼的问题,调试在此毫无用处,只能使用print大法。但是print本身会影响设备对线程的调度,有的时候加入print后bug无法复现...以作者浅薄的知识和愚笨的大脑,只能总结出以下几点

  • WA && 不可复现的RTLE
    不可复现的RTLE即在本地没问题,但是交上去就是不能通过。一般WA(仅限于线程问题的WA)和RTLE的问题使用print大法行不通,因为print加多了,会影响对线程的调度,使bug无法复现。盲改也不行,改动多了也会影响调度。此时最好借助评测机(大佬开发的面向大众的评测机),一般在大型评测机上能够复现错误,然后使用瞪眼法,找到问题所在地,自己分析问题原因,进行针对性的解决。

  • 可复现的RTLE
    可复现也分为线程问题和非线程问题,一般能够自己分辨出。线程问题,线程错误结束、人没送完、人被困电梯等问题,同上,根据错误问题进行针对性解决。非线程问题,如调度策略不够好导致用时过长,如前面提到的同一时刻涌入大量请求,则考虑改进调度策略。

  • CTLE
    主要是轮询。在循环中添加print定位轮询位置,具体内容见Part2 1.3 bug分析

Part5 心得体会

本单元任务是作者第一次接触到多线程处理,对于初学的我来说是一个不小的挑战。理解多线程原理和使用方法让作者感到崩溃,面对无法复现的bug令作者感到更加崩溃。所幸有学长学姐的博客帮助,助教们的工具帮助、debug方法帮助,研讨课大佬的耐心解答和分享,交流群里各位同学实时的、耐心的解答,作者也算是完成了单元任务,并且在一次次尝试中逐渐理解掌握了多线程的原理和使用。

对于线程安全。我们只需要对多个线程共同访问的数据加锁即可。一开始我对请求队列的全部方法加锁(synchronized),因为只有waitQueueprocessingQueue(均为RequestQueue类)被多个线程共享。waitQueue由输入线程和调度器线程共享,processingQueue由调度器线程和电梯线程共享。这样的加锁可以解决前两次作业的全部问题。第三次作业因为涉及双轿厢的防碰撞,以及在调度策略中也需要调度器对电梯状态进行频繁地读。即其他电梯线程、调度器线程会对电梯状态进行读,本电梯线程会对电梯状态进行写。这时候使用synchronized对方法加锁不是一个好的选择,因为读操作太频繁了,使用synchronized会损耗很多不必要的时间。因此选择使用读写锁,对读写进行精确的加锁,保证性能和正确性。

对于层次化设计。再次感谢实验代码!输入线程、调度器线程、电梯线程的组合在三次作业中没有进行改变,可以解决所有问题。这也体现出生产者-消费者的思想,同时具有优秀的可拓展性。

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

301

社区成员

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

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