第二单元总结
在对本单元的内容进行分析前,让我们先回顾一下本单元的大致结构:生产者-消费者模式,对应到我们的电梯系统,也就是请求-电梯模式。请求通过IO输入,经过缓冲区waitingQueue,以一定的规则分配给电梯进行运送(也就是我们所说的调度)。这个大致的结构可以被概括成以下这个图表,聊以在没有进行结构分析前,进行背景铺垫。
一、同步块的设置和锁的选择
同步块的设置
同步块的设置是为了保证所有临界区可以实现互斥访问,即在多个线程访问同一块共享资源时,可以避免由线程不安全导致的数据错误以及异常错误。因此,存在临界区的地方,为了保证线程安全性,就必须设置相应的同步块。
而在本单元的作业中,涉及到数据读写的部分,在生产者-消费者模式下,应当是他们的共享数据:生产者不断生产需求,消费者获取需求并进行解决(其实也是实验课上提到的客户-工人模式)。
所以在本单元作业中,我选择将同步块设置在调度器和处理IO线程共用的缓冲区。与此同时,我还分别将电梯的载客容器sendingQueue、电梯的待处理请求队列processingQueue设置为线程安全类,避免大量同步块使代码可读性、可维护性、可扩展性下降。
除了保证线程安全以外,同步块还可以用来避免轮询。我们的调度器并不是在一个时刻获得的所有请求,而是时刻等待着输入。但如果时刻保持着线程的苏醒,就会进行轮询,大量占用了系统资源,通过设置同步块,我们可以调用同步对象的wait以及notify方法,在没有可用资源时,让一部分线程睡眠,从而避免忙等待。
锁的选择
我们在课堂上主要涉及了两种锁的选择。
第一种便是通过synchronized语句来在特定代码区块中为指定对象进行加锁,这种方法较为简便通用,只需要将synchronized(Object)中Object替换成你想要施之以锁的对象就好了,在其中代码段执行notify语句、执行完毕时或是线程发生异常时JVM会释放该锁。但对于同一代码段加入多重的synchronized块可能会导致死锁现象,也会使代码变得不可描述(。
第二种则是lock&condition组合。我们可以实现我们特定的锁,比如读写锁。对于我们想要实现的线程同步与互斥,其实可以做剪枝优化的。比如只有在不同线程进行读-写、写-写时,才需要我们设置临界区进行同步互斥,当读-读时我们并不需要加锁——这会或多或少降低我们程序的性能。因此我们可以实现读写锁,在需要加锁的代码段前加一句Object.lock(),也可以通过条件语句Condition.await()/signal()(可以唤醒指定线程)进行类似wait()/notify()(只能随机唤醒线程)一样的操作。
在竞争激烈的情况下,lock的性能表现更优,但是抵不过synchronized更省事(bushi
在本单元的作业中,我选择了实现简单、效果稳定synchronized块,从而更好的将注意力放在电梯的设计和调度上。
二、调度器设计
功能设计
为了程序的可扩展性,本单元作业我从第一次作业就开始着手编写调度器。在三次作业中,调度器的任务从简单的处理缓冲区与任务处理区的关系(第一次),再到简单的单一类型电梯的调度(第二次),最后到对于不同类型电梯的较为复杂的换乘调度(第三次)。
最开始的调度器就是一个简单的缓冲区,将主请求队列中,取出楼层最高的的请求传递到对应电梯的待处理队列中。
第二次的调度器在增加电梯的条件下,将当前请求分配给不同的电梯。主要原则是保证每个电梯都能动起来、每个电梯中请求队列+运送队列人数均匀。
第三次的调度器在第二次的基础上,对调度器进行了扩增,将请求先分配给C,如果C不符合相应条件,再分配给B,如果B也无法运载,再分配给A。
调度器与线程的交互
调度器与线程的交互主要是读取IO线程、电梯线程的共享数据,从而做出一些反应。在调度器中,通过共享主请求队列以及每个电梯的信息,我们可以实现一定的调度分配策略。在根据一定的策略分配信息以后,我们还能够通过这些共享数据来为其他线程加入一些请求。 主要分为以下两类:
1.调度器线程 从IO线程中获取新请求
2.调度器线程 向电梯线程中投放请求
三、可扩展性
UML类图
UML协作图
扩展性
让我们通过四个可能的需求增加来考虑可扩展性:
假想1:扩展电梯种类
通过增加Elevator类的子类进行扩展,可扩展性好。
假想2:增加电梯功能
通过Function扩展接口实现
假想3:实现其他策略
在调度器中增加相应的条件语句(可扩展性差
假想4:修改部分类型电梯的部分功能
需要修改原来代码(不符合开闭原则(但我觉得情有可原...
总的来看,通过继承和接口(java多态)的运用,在本单元作业中,程序的可扩展性还是得到了较高的提高(相较于第一单元),但策略类还是没有实现较高的可扩展性。
四、bug分析
未通过的公测用例
第一次作业:一个Night模式下的测试点超过了ALS策略所需时间。
第二次作业:一个Random点在170s才结束输出,自己程序不够快结果超过了210s (RTLE)
第三次作业:换乘时候开关门忘记加一个if语句,强测挂了一半(悲)
互测被发现的bug
感觉本单元大家互测的积极性都不高(可能因为冯如杯OS比赛课程之类的压力比较大吧)
前两次互测侥幸逃过一劫
第三次互测 人在C Room 大家都躺平
五、bug查找策略
测试策略
本地测试
除了第三次作业以外(偷懒没测结果就悲剧了呜呜呜),对于其他的作业尤其是刚开始的作业,做了一些基础的功能测试,来测试IO线程和调度器对于同时输入的反应和电梯的基本功能。可以拿别人的评测机进行评测,但感觉意义不大,自己很容易沉迷到那种使用黑盒的变态快感中(误。
互测
第一次作业还做了一次尝试,后来两次作业就没有再管互测,大概是下载同房间同学的代码静态分析一遍后发现看不出什么问题(。。)就跳过了。
线程安全相关问题
在中测的时候遇到了很奇怪的问题,在研讨课上纪老师称之为”薛定谔的bug“。在第三次作业中测的过程中,有一个测试点出现了RTLE,但是在再次提交的时候就通过了测试样例,且强测中并没有出现该种情况。奇怪的是我的所有容器类,要么全部实现了线程安全方法,要么调用的是java库函数中的BlockingQueue、ConcurrentHashMap等线程安全类...
与第一单元的差异
第一单元的测试样例和测试结果的生成和获取门槛都比较低,大家更容易参与进来。多线程的输入输入处理和bug判定对于我来讲还是门槛有点高。还有就是大家都不刀的话,会产生一种心理效应,对于群体来讲,如果达成了某种共识,比如多线程互测比较难,刀很多刀也刀不到人,那么后来者(比如我)可能也就跟风随流了。不过这单元比较令人开心的是,自己学会了阅读别人的代码,学习到了很多东西,感觉还不错。
六、心得体会
线程安全
为了保证线程安全,我将sendingQueue、WaitingQueue等对象实现为线程安全类。并在IO线程、调度器获取请求、每个电梯获取请求时设置了synchronized块,在其中设置wait()来避免线程不安全的情况。其实脱离作业来讲,线程安全核心还是同步互斥问题,对于共享数据的管理问题。在本单元中,我们主要学会了同步块、读写锁等方法,在OS课学习理论的时候能够更加容易理解(确信)。
层次化设计
在本单元中,弥补了上一单元没有怎么使用继承和接口的遗憾。通过上面的UML类图可以看出,相较于第一单元更多地使用了多态。设计更加合理和清晰。美中不足的是代码本身并不简洁,过多因为数据结构选择不合理导致的流处理的使用使代码十分丑陋。以后对于数据结构的选择会更加慎重。
© 著作权归作者所有
发表评论