OO第二单元总结
- 概述
本单元是多线程的练习单元,以电梯调度问题作为多线程的应用场景,以此来让我们熟悉常见的生产者-消费者多线程设计模式,同步代码块和锁的选择。下面我将从6个方面来总结第二单元的学习。
- 同步块与锁的选择
在多线程的编程中,临界区的划分非常重要,恰如其分的同步块划分和锁的选择可以保证多线程程序的安全性的前提下,尽可能提高程序的效率。
- 作业中的设计
第三次作业基本上继承了前两次作业的临界区划分,并在此基础上添加了新的同步块,因此我将以第三次作业为例介绍我的同步块设计。
- input线程
input线程为输入线程,其作用为读取并解析输入中的请求,往调度器类中添加电梯或请求,由于调度器中队电梯队列访问和调度器的请求队列的访问在同一块区域,因此此线程中的锁对象为schedule的等待队列,此对象是input修改的对象。
- schedule 线程
调度器类的第一个锁为调度器的等待队列,块内区域涉及到对于等待队列的判空和遍历访问。调度器的第二个锁锁嵌套在该同步块中的分配策略中,锁对象为电梯中的主请求锁,设计该同步块的目的是为了安全地取电梯当前的主请求,防止在策略制定期间主请求发生变化。调度器的第三个同步块为电梯请求添加时的同步块,锁对象为电梯申请的等待队列锁,防止电梯线程和调度器线程同时对等待队列进行访问。
- elevator 线程
电梯线程中的第一个锁对象是自身对象,只有获取到自身对象的锁才能对电梯的等待队列和乘客队列进行操作。第二个锁对象为主请求的锁,所有对主请求进行更新的代码块均为同步块。第三个锁对象为buffquene请求队列的锁,用于换乘时吐出新请求到缓冲队列中。
- Buffquene 线程
该线程的第一个锁对象为自身的等待队列,当获取到锁后,如果自身的等待队列不为空,则会继续尝试获取调度器等待队列的锁对象,以添加请求。
- 调度器设计
第一次作业只有一部电梯,理论上来说不需要调度器也能达到正常的功能,不过为了从一开始就搭好较为完整的结构,我在形式上也设计了一个调度器类。在之后的两次作业中只需要对调度器的电梯队列以及分配策略进行修改。调度器的功能为从输入线程中获取新到达的乘客请求,并通过电梯在这一时刻的静止状态制定策略进行请求的分配(第一次作业中只有一部电梯就直接投放),分配的具体交互方式为获取目标电梯的锁并将请求添加到目标电梯的等待队列中。
在输入结束之后,由输入线程通知调度器线程,再由调度器线程通知到每部电梯,电梯在接受到结束信号后,处理完自身请求队列中的请求之后就会自动中止。
- 功能设计与性能设计
- UML类图
![image]()
- UML顺序图
![image]()
- 分析
- 功能上,我设计了4种线程,包含了读取并解析输入的输入线程、对请求进行调度的调度线程、电梯运作的线程、用于缓冲换乘生成的新请求的缓冲线程。在可扩展性方面,对于更多类型电梯的添加只需要继承电梯的公共父类,然后在自身的初始化过程中对载客量、运行速度进行个性化配置。对于不同的主请求获取策略也可通过继承decision类并添加至目标电梯来实现。
- 性能上,为了解决结束条件的轮询判断超时问题,我设置了0.2s的读取间隔,牺牲了部分电梯运作的性能。在调度算法上,我仍然采取的是静态的调度算法,即根据请求到达时刻电梯的静止状态进行三个维度(电梯剩余容量、电梯执行请求的方便度、电梯的速度)的打分来进行请求的分配,这样调度存在的问题时对未来情况的适应性比较差,不能及时调整分配策略,而且即使暂时抛开未来请求的影响,从三个维度打分也存在着一些问题,如怎么权衡各个维度的比重,如何何有区分度地分开各种情况,因此我认为在性能上本单元的作业还可以进一步完善。
- 程序bug分析
在三次作业的公测与互测中,我出现bug的位置集中在调度器类和电梯类当中。bug类型可分为两种,一种是结束条件判断,另一种是程序死锁。
- 结束条件判断
第一次作业和第二次作业的结束条件比较简单,输入完毕后就可以关闭调度器线程,电梯请求队列和电梯主请求为空时就可以关闭电梯线程。但第三次作业由于涉及到了换乘,电梯中的请求可能还会被吐到缓冲队列中等地加入调度器的等待队列进行二次调度,如此一来结束条件的判断就变得比较复杂:输入结束后,需要等到所有的电梯等待队列、缓冲队列、调度器等待队列都没有请求时将调度器、缓冲器、电梯线程同时关闭。第一个bug是采用轮询的方式检查各电梯的状态,在全部请求到达完毕后间隔较长时间再输入结束标识符容易cpu超时;为了解决这个问题,我采取了间隔轮询的方式避免密集的轮询方式,每一次轮询若未达到中止状态,那么超时时间为0.2s的wait状态。这个解决办法的缺点是浪费了一些性能,不能准确地在可以结束的临界时刻进行线程的终结。第二个bug是C电梯的设计不当,导致C电梯接入了目标请求不在C电梯到达范围之内的请求,最终由于乘客不能始终下电梯造成了程序超时。
- 死锁
在多线程之中,死锁可以算得上一类比较经典的bug。在第三次作业的最初设计中,我没有设立缓冲队列,换乘的新请求直接通过获取调度器锁的方式来二次调度。经过分析,这种设计在性能和功能上都不是很好。一方面,当电梯线程等待调度器锁的时候是被阻塞的,无法继续运行以及接受新请求;另一方面,由于调度器类中有获取电梯锁的操作,在特定的时候会造成死锁问题。为了解决这个问题,我设立了buffQune这个缓冲队列,换乘产生的新请求会先被加入到缓冲线程的队列s当中,缓冲线程再伺机获取调度器等待队列的锁对象进行新请求的添加。
- 发现其他人bug采取的策略
- 极端数据
对于random,即采取跨度很大,时间集中的数据,如1到20层的请求在很短的间隔内来上8,9个,对于那些拿了请求就跑的电梯杀伤力很大。
- 换乘考量
对于第三次作业,可以安排特别依赖换乘的数据,如1到20、1到18层的请求,如果没有灵活地运用B类和C类电梯的速度优势进行换乘,则有可能超时。
- 与第一单元的差异
本单元与第一单元相比,测试方法不局限于在同一时刻投放全部输入,而是要较为严格地把握投放时间才能起到hack的效果。
- 心得体会
刚接触多线程的知识时,个人是比较恐惧的,因为习惯了程序单个线程一条一条执行代码的模式,对于多线程的并发执行、多线程对共享数据的访问和修改、多线程的设计模式都比较陌生。第一次作业虽然事后看来是比较简单的,但在当时做的过程中是非常茫然的状态,其后通过老师的讲授和大佬的设计分享才逐渐摸到门道,之后的两次作业稍稍轻松了一些,更多的是对于设计模式和架构的思考和学习。总的来说,这个单元的训练让我初识了多线程编程。
posted @
2021-04-27 09:11
弈~忆
阅读(
51)
评论()
收藏
举报