BUAA-OO-第二单元作业-电梯初体验
---恢复内容开始---
摘要 && 前言
oo课程的学习已经进行了8周,对oo的了解也有了进一步加深,也终于体验到了传说中电梯的编写。
不论是通过研讨课的讨论或是课下与大佬们的交流,发现自己与其他大佬们的差距是很大的,这种差距不仅体现在代码能力上,也体现在对于oo这门课的态度与积极性上,把自己的目标放在通过中测这种态度是远远不够的。
说回第二单元作业,在这三次作业中分别编写了单部多线程傻瓜调度(FAFS)电梯的模拟、单部多线程可捎带调度(ALS)电梯的模拟、多部多线程智能(SS)调度电梯的模拟,在三次作业中初步体验了多线程程序的编写,踩了很多坑也收获了很多知识。
作业代码分析
第一次作业代码分析
设计思路:
由于本次作业的调度方式为FAFS,即先来先服务,一种很自然的设计思路是创建一个电梯线程、一个调度器线程,其中调度器保护一个queue的线程安全性,调度器不停地从标准输入读入请求并将其放入queue中,而电梯线程每次执行完一个请求后,向调度器发出已经执行完毕的信号,调度器将queue中第一个请求发送到电梯线程中。核心在于每个类只做每个类该做的事情,电梯类只负责执行请求,而调度器只负责将请求传达到电梯类。
设计架构也非常简单,只需要按照生产者消费者模型,将input类作为生产者,调度器作为托盘,电梯作为消费者即可。
UML类图如下:
代码复杂度分析如下:
可以看到,由于本次作业的结构较为合理,各个类中的方法复杂度都不是很高,成功降低了模块的耦合。
第二次作业代码分析
第二次作业要求在第一次作业的基础上实现ALS调度,即考虑了捎带的情况,主要思路为在电梯空载时选取最先到达的request作为主请求,在执行主请求时每到达一层楼都判断一遍是否有可稍带的请求。我在第二次作业中为了简化调度器的算法,为每个楼层建立了一个queue,包含了起始楼层为该楼层的请求,这样电梯到达某一层时只需遍历该楼层的queue即可。
设计架构仍为生产者消费者模型,input类作为生产者,tray作为托盘,elevator作为消费者,这次作业与上次作业相比,难度提升主要在queue线程安全性的保持难度升高。
UML类图如下:
代码复杂度分析如下:
可以看到,bringUp与bringDown两个方法的复杂度相当高,这也是本次作业去耦合化失败的一个地方,在这个方法中实现了从起始楼层到终点楼层所有的开关门以及捎带操作,复杂度相当之高,这对于oop来说是十分不合理的,在第三次的作业中也为此付出了代价。
第三次作业代码分析
第三次作业的要求为实现多部多线程智能(SS)调度电梯的模拟,难度相比于前两次作业有了一个很大的提升,对每个电梯可到达的楼层、速度以及载客量都做出了限制,可优化性方面有很大的空间,我的思路主要还是ALS电梯的思路,为每一部电梯分配一个主请求,如果可以直达,则终点即为到达楼层且在过程中寻找可稍带的电梯即可,如果不能直达,则将终点定为换乘楼层,且在过程中判断捎带。
本次作业与前两次作业在架构上的区别主要在于消费者有三个,对于托盘queue的线程安全性再一次提出了挑战
UML类图如下:
方法复杂度分析如下:
可以看到由于ALS调度算法作业中耦合性就比较高,对于难度更高的多电梯调度作业,复杂度只会直线上升
在第三次作业中还出现了一个问题,那就是在评测机上很多测试点跑出来都是RE,在查看讨论区后发现这是由于没有使用wait与notifyall方法,导致线程一直在执行,使得CPU时间较高。
程序bug分析
第一次作业逻辑较为简单,强测与互测均为发现bug,自己在写程序时发现需要保证queue的线程安全性,如果同时对queue进行读取与写入操作,则会导致取出一个null指针。
第二次作业在逻辑难度方面依然较为简单,bug的主要来源是在输入数据量较大时会导致RE的产生,和上述提到的一样,这也是线程长期占据CPU导致的,解决办法为使用wait和notifyall方法,使线程主动让出资源,减少线程对CPU的无谓使用。
第三次作业是逻辑复杂度较高的一次作业,也是我出错最多的一次,主要问题在于RE,仔细查看程序后发现是我的电梯类在run方法中产生了轮询,虽然使用了wait和notifyall方法,但是由于是对于存放request的queue上锁,从而导致可能有两个线程不停切换阻塞态与就绪态的可能性。通过自己编写评测机的方法只能保证逻辑上的严密,但对于可能出现的暴力轮询还是无能为力,因此对于线程的阻塞、唤醒以及对象的同步与上锁还是需要好好理解。
Hack策略
因为多线程程序的bug具有偶发性和不确定性,我的hack策略主要是通过python程序大量跑某一测试点来检测程序的线程安全性问题,但还是需要人为判断程序的逻辑问题(每个人是否到达了目标楼层等),我的python程序也还没有定时投放的功能,只能在第零秒一次性输入所有数据,对于互测来说也是很不方便的。
发现的问题大都集中在线程的不安全性以及某些情况下的死锁情况。
总结
经过第二单元的三次oo作业,对于java多线程程序的编写有了一个初步的认识(也充分体验到了oo会把你欠下的都还给你这句话),体会到了架构以及线程安全的重要性。
线程安全:
在写程序之前就应该想好可能出现的线程安全问题以及解决办法,对于可能出现的轮询、死锁等情况都要充分考虑清楚。在我的三次作业中,出现的共享变量都是调度器对象的queue,我的保证线程安全的方法是在有对象对queue进行操作时对queue对象上锁,保证每次只有一个线程可以对queue进行更改,但是我忽略了三个电梯线程对于queue请求request的轮询情况,这是一个很大的失误,在今后的作业中需要避免。
设计原则:
oop的一项重要设计原则在我看来就是要降低耦合,每个类只负责自己的事情,任何对象的任务太多都可能导致程序的面向过程味道浓厚。在多线程程序中,另一个重要的原则就是减少共享变量的数量,对于共享变量的访问与读写操作一定要保证线程安全,除此,减少锁的嵌套也是一种避免死锁的有效手段。在我的第三次作业中就因为锁的嵌套出现过死锁的情况,弱测过程中会随机性的出现一个测试点跑满cpu_time都没有输出的情况,最后通过大量测试在本地复现。
oo作业到现在已经过去了一半,在这六次作业中也发现了自己很多问题,不论是学习方法、态度亦或是代码能力,但抱怨对于这些也无济于事,希望在后面部分的作业中能吸取之前的经验教训,向着写高质量代码的目标努力。