在多线程单元我进行了很多思考与尝试,在探索的过程中也有较大收获。在这一次作业中,我将回顾U2各次作业的完成情况与体会。为充满精彩的U2单元画上最终的句号。
第五次作业
同步块的设置与锁的选择
本次作业中逻辑较为简单,由于乘客进入电梯时指定要去的电梯,分配器只需要按需分配即可。线程之间的配合逻辑并不复杂。只有输入线程-分配器线程的需求传递和分配器线程-电梯线程之间的需求分配,运用两个生产者-消费者模型即可解决。
在两个需求传递的类中控制同步即可,所以我并没有使用同步块。只是在输入与分配器之间的waitQueue与分配器与电梯之间的reqQueue类的方法中加锁来控制。
调度策略
本次作业中并不设置分配器分发策略(因为需求中指定电梯),只涉及到电梯运行策略。
我尝试过对Look策略进行改进,在思考的过程中也意识到Look策略用方向优先避免个别用户等待时间过长的优势。最终还是选用了朴素Look策略。这种策略在运行时间,等待时间上都有良好的表现,需要的操作也较小,节省了耗电量。
调度器的交互逻辑也较为简单。它与输入线程共享总表waitQueue,与电梯分别共享子表ReqQueue,只需要控制各共享对象同一时刻只有一个线程访问即可。
UML类图与时序图
变与不变
通过分析,不难分析出电梯的容量,速度等是易变的,应该用变量控制,最好不要在程序中直接写为常数。
不变:都可以变!迭代大有可为!(丧心病狂版)
啊其实我觉得电梯的运行策略就是不变的。是一个选定后就比较稳定的组成部分。
Bug分析:
比较简单,没什么Bug。
第六次作业
同步块的选择
本次作业中我运用了影子电梯思想和量子电梯思想进行了优化,由于一些特殊考量,架构有了一些复杂化的调整。
在我的讨论区帖子中也有所提及,为了保证影子电梯计算的精确性,我要求影子电梯在计算时拿到所有电梯的锁。也就是添加了如下的同步块:
public void distribute(Person person) { synchronized (requestTables.get(0)) { synchronized (requestTables.get(1)) { synchronized (requestTables.get(2)) { synchronized (requestTables.get(3)) { synchronized (requestTables.get(4)) { synchronized (requestTables.get(5)) { // do something…… } } } } } } }
写法有些丑陋,这也是本次作业中唯一的同步块。
本次作业的架构如图:
与第二次作业的差别主要在于
-
将电梯的参数等信息保存在ReqTable中,使电梯更像一个纯粹的自动机
-
请求表可以向waitTable添加请求(Reset造成,但是是由电梯线程执行的)
除了架构以外,还涉及到线程正确结束,分配策略的合理性与输出的正确性等细节,并无理论上的最优方法,尽可自行探索,尽兴就好。
锁的应用与第五次作业保持相同。
调度器设计
本次作业中我选取了影子电梯策略,对电梯进行深克隆后模拟运行,计算所用时间进行比较。调度器在进行深克隆时要拿到所有ReqTable的锁,在数据规模较小时,这里的开销是比较明显的劣势。
影子电梯的策略在时间上优化效果较好,但未考虑耗电量等因素。
UML类图与时序图
变与不变
电梯性能参数果然变了,用属性进行控制是好文明。
请求的处理模式也可以改变,这是我没想到了,但电梯的策略执行模块依然保持不变,这说明了我没进行模块分割的合理性。
Bug修复
第六次作业也比较顺利,没什么Bug
第七次作业
同步块与锁的选择
本次作业新增了双轿厢电梯需求与新的重置请求,而新重置请求的处理完全可以参考上次作业的实现,故而只需要处理好双轿厢运行:正确运行分配与防止碰撞即可。
同步块与锁的选择继承了上次作业,本次作业中电梯可能分为双轿厢。为了简化逻辑,我选择初始时创建12个线程。
分配器调度时的上锁也添加到了12把。
public void distribute(Person per) { synchronized (requestTables.get(0)) { synchronized (requestTables.get(1)) { synchronized (requestTables.get(2)) { synchronized (requestTables.get(3)) { synchronized (requestTables.get(4)) { synchronized (requestTables.get(5)) { synchronized (requestTables.get(6)) { synchronized (requestTables.get(7)) { synchronized (requestTables.get(8)) { synchronized (requestTables.get(9)) { synchronized (requestTables.get(10)) { synchronized (requestTables.get(11)) { int id = bestId(per); requestTables.get(id - 1).addRequest(per); } } } } } } } } } } } } }
而在架构与线程交互的方法上,本次作业与上次作业并没有明显的区别。
在进行电梯模拟时,为简化计算,双轿厢电梯只模拟其中一个轿厢,之后通过一定的折算方式估算时间。
同时,双轿厢电梯中乘客来到换乘层之后重新返回waitTable重新分配。
Bug分析&防碰撞抉择
本次作业在通过中测之前出现了一个死锁问题,与采取的防碰撞策略有关。
原来的防碰撞参考了讨论区中讨论帖的方法,利用类似信号量的机制,进行换乘区控制。
具体方法是,在双轿厢中添加共享对象flag,进入换乘层之前在flag中检查置为标志,若置位为真则等待,否则则将置位标志置为真且进入换乘层;离开换乘层时将标志置为为假并执行notifyAll
这种方式在影子电梯架构中出现了一个冲突,即拿12把锁的冲突。
A轿厢准备离开换乘区,在move的移动过程中wait400ms,分配器拿到锁。同时,B轿厢准备进入换乘层,在flag中等待。此时造成了死锁!
这里我最终进行了架构调整,只在flag中维护标志位,一旦标志位为另一轿厢电梯,则在flag外等待一小段时间再访问。避免了死锁发生。
修改之后强测就没出现什么Bug了。
多线程debug方法
帮助同学debug的经历比较丰富,故我也积累了一定debug经验。
定位多线程bug时,定位死锁问题应使用print方法,在代码各个位置输出调试信息,观察输出的缺失情况。
而对于线程安全的问题,似乎还是应该依靠代码走查。
心得体会
经过三次作业的实践体会,我认为,多线程程序设计应“谋定而后动”。先确定架构设计,线程交互方式,排查死锁问题和线程安全问题。确定没有架构问题之后才可以继续编程,如此就可以在很大程度上避免线程问题,类似问题一旦发生则修改较为困难所以应在一开始就保证设计的正确。
此外,我也在这一单元中体会到了探索性能分的快乐,思考尝试并获得成果的成就高很高~