OO第二单元的题目是电梯调度,该单元的主题是多线程的编程,不同于以往的单线程编程,多线程程序需要考虑多个线程访问共享数据时的一些不可预测的变化,涉及到数据的同步和互斥访问,并且由于多个线程同时执行的原因难以进行单步的调试,只能依靠printf打印相关信息进行基本调试,debug难度大大增加。关于多线程的实现老师为我们提供了两种方法,一是继承Thread类,二是使用Runnable接口。我在第二单元的三次作业中都是使用继承Thread类的方法,该方法较为简单,只需要调用start方法就可以启动线程,运行run函数,run函数结束则线程消亡。
一、作业分析
第一次作业
第一次作业较为简单,只有一部电梯,使用FAFS傻瓜算法一条请求一条请求地执行即可完成任务。我采用了一个较为取巧的算法,即在Main函数里获得输入,当获得了一条输入就启动一次电梯线程来完成指令,再获取到下一条指令,因此不需要指令队列来存储指令,其实质上等于单线程程序,因此也不需要考虑线程安全问题。但是此种方法可拓展性极差,后面的作业几乎没有任何可以复用的地方,本质上来说是一次不合格的多线程作业。
类图
从统计来看各个方法的复杂度都比较低,在此就不多做分析。
第二次作业
第二次作业与第一次作业相比最大的不同在于增加了一个捎带要求,电梯数量不变,因此傻瓜调度不再可行,第一次作业的架构也无法复用,所以必须重新构建结构和算法。我的思路是除了主函数之外新建三个类,分别是Input,Elevator和Request。Request类用来存储指令信息,之所以特意设立一个Request类是因为捎带算法有运行方向的需求。Input类专门用来获取输入并存入共享队列中,每次获取一次输入或者结束了输入都需要唤醒电梯,结束输入还需要传递一个输入结束的信号给电梯。Elevator线程就是具体的电梯线程,结束条件是输入结束并且指令队列为空,当输入未结束但无指令运行时wait电梯,等待唤醒。完成捎带的方法也不复杂,只需要在电梯到达每一层遍历一遍指令队列寻找当前楼层出发且同向的指令即可。
类图
从统计结果上看主要是start_end方法复杂度远大于其他方法。该方法实现了捎带,有多个循环遍历过程,因此复杂度较高。
第三次作业
第三次作业比起前两次作业,难度增加了许多,从一部电梯到三部电梯,各个电梯有相应的可到达楼层,运行速度各不相同,还存在需要换乘的情况。针对这次的作业,我在第二次作业的基础上增加了一个调度器线程,用来给电梯分派指令,三个电梯线程在调度器内启动,调度器和输入在main函数里启动。对于指令的派发,我是先判断能否直达,若能够直达则按照ACB的判断顺序决定分派给哪部电梯,若是不能够直达则需要换乘。关于换乘的算法,我将1楼和15楼作为中转楼层,电梯离哪个楼层近就去哪个楼层中转,将指令拆分为两个,后半部分指令存入前半部分指令中,并设置该指令的中转标志,使得后半部分指令能够在前半部分指令执行完之后再加入指令队列,实现中转。
类图
从统计来看复杂度主要集中在调度器和电梯中,以后作业应该尽量避免将算法在一个函数内完成,实现代码的简明扼要。
二、bug分析
第一次、第三次强测互测均无bug,这里就不过多分析,第二次作业有一个合并修复的bug,问题主要是Input写入指令队列是没有给指令队列上锁,引发了线程安全问题,导致wrong answer。在细节实现的过程中出现的bug基本也都是加锁问题。
三、互测
互测过程中我主要还是先看对方代码访问共享对象时是否是线程安全的,若不是则构建相应的样例hack。
四、总结
这一单元的三次作业在算法难度上低于第一单元的作业,主要考核的是我们对于多线程的一些基本的使用规范和注意事项,考察我们对于线程安全的理解,理清楚思绪之后其实一次作业完成时间并不长,若是第一次作业就有了良好的架构那完成后两次作业将非常轻松。多线程的设计要点是可分离性,即各个线程应各司其职,尽量减少不必要的交互,交互对象的互斥访问也不可遗漏。
SOLID原则:
1、单一责任原则:电梯内有类似open、close、in等函数符合单一责任原则。start_end函数较臃肿,实现了多个方法
2、开放封闭原则:代码复用性较差,第二次作业完全重构,第三次作业也调整了一部分结构
3、里氏替换原则:继承较少使用
4、接口分离原则:未使用接口
5、依赖倒置原则:调度器里有电梯,电梯可以唤醒调度器