BUAA 面向对象课程 第二单元总结
第二单元进行了多线程编程设计训练。
第五次作业:模拟单部多线程电梯的运行。
第六次作业:模拟多部同型号电梯的运行,并要求能够响应输入数据的请求,动态增加电梯。
第七次作业:模拟多部不同型号电梯的运行。型号不同,指的是开关门速度,移动速度,限载人数,以及最重要的——可停靠楼层的不同。
一、多线程编程总结分析
-
本单元作业,最初的设想就是封装好相应的线程安全类,这样外部调用的时候就不需要过分考虑手动确保线程安全的问题。在最终的代码中,synchronized块也确实只在设计的两个线程安全类中出现,在外部调用的时候并没有手动加锁。
-
对于线程安全类的实现,要在类中设计好内部的共享对象,并在有并发需求的方法中加入锁相关的语句块。
可以有标志位的设计,类似于 Poison Message 的思想,实现某些初始状态的进入以及状态接受到信号而做出相应的响应。
-
锁选择了synchronized语法,这样对于读和写操作一视同仁,同一时刻只能有一个读或写操作(对于加锁的对象)。实际上可能会出现多个读取请求,实现代码的时候可以尝试使用lock机制,让读操作能够实现同时发生,可能会提高计算效率。
二、调度器设计
第五次作业
单电梯,直接把人从总的等待队列放入那个电梯自己持有的队列里。
第六次作业
对于Random,对人计算权重,权重以电梯运行方向以及电梯当前楼层与人所在楼层为指标,将人放入那个权重最优的电梯的内部等待队列中。
对于night及random很相似,都是每次循环都从总等待队列中取出所有人,先排个序,然后放入电梯内,此时不计算权重而是放入某个电梯,待电梯满则放入下一个电梯。区别在于两者的排序,一个是由大至小,一个是由小至大。
第七次作业
不换乘,根据请求特征将人分配到合适的电梯中,逻辑较简单。
三、架构设计
UML类图
架构在迭代开发中无较大变化。如图所示,Main会创建两个线程,一个是输入线程InputThread,另一个是由工厂方法获得的特定调度器Scheduler。而电梯线程的创建,电梯的管理则在Scheduler中。除此之外,Scheduler还进行人员的分配,将等待队列WaitQueue中的人由相应的算法/策略分配到某个电梯内部等待队列(SmartQueue)中。
上面所说的是整体的架构,而另一个非常重要的部分则是电梯类对模拟运行的实现。
在这里我选择了类似状态机的实现。(但后期我思考了一下,电梯的运行也可以想象成线性的执行,只不过某些步骤可能会跳过/空行为,如某楼层不停靠。基于这种设想,写一个状态机可能麻烦,而且更费脑筋?)
状态转换图:
图中略去了十分重要的条件判断,此处简单的叙述一下。入口是WAIT状态。对于WAIT状态,它代表的涵义是这个瞬间,电梯失去了目标楼层(在ALS调度中也就是指主请求)。OBSERVE状态下进行判断,根据电梯内的乘客以及电梯门外等待人员的情况,判断是否需要开门。重要的一点是,在RUNNING状态开始时,进行目标楼层是否丢失(已到达)的判断。(在我最初的状态设计中,这里的判断出了问题,在公测中并没有直接测出)
讨论课中所介绍的ALS,LOOK,SSTF算法,在我的实现中则是在WAIT阶段,获取一个新的目标楼层这个方法。我采用了SSTF。如果想切换算法,则只需要对这单独的一个算法进行改变,并且这个算法是封装在线程安全类SmartQueue中的,修改不会破环整体架构,对外实现隐藏。
协作图
主线程
调度器线程(以某一个调度器)为例
电梯
四、分析bug
第五次作业出现了一个问题,对于morning模式,只需要在第一次接人&&此时处于第一层时需要等2s,其余楼层开关门只要0.2+0.2s,而公测版本却在非第一次到达第一层的时候停留2s。实现是对这个细节处理的不好。
第六次作业Random模式有个极端数据,30人同时到达一楼,算法中忽略了对电梯已满的判断,此时权重值应该最差。
另外发现了状态机有个设计问题,对于目标是否已完成的判断应在RUNNING状态判断,而不能错误的设计成OPEN_CLOSE阶段的末尾判断。错误的写法会带来的问题是,假如电梯在前往目标楼层去接人的时候,在途中捎带了许多人让电梯已满,则到达目标楼层的时候会出现循环。
无线程安全问题发生,两个线程安全类已经封装好了对队列的各种操作。
五、分析他人bug
互测中并没有去hack其他人。在第三次的互测中,有人的程序出现了线程安全结束的问题。对于某些算法来说,调度器会从主等待队列中获取请求放入电梯内部队列,但同时电梯也会把不合适的请求塞回主等待队列中。在这种情况下,逻辑上就会出现循环依赖。而这样就无法继承第二次作业所使用的线程结束方法(比如设置守护进程、设置标志位,设置Poison产品等)。我想了一下这种情况的解决方案,认为可以设计两个标志位,标记主队列和电梯,当输入结束并且电梯也无产品同时主队列为空,调度器才可结束运行。这样或许可以解决线程安全结束的问题。
六、心得体会
多线程编程,对于共享对象的设计,同步执行代码的实现,还是比较费头脑的。但是有比较成熟的模型(设计模式)以及他们实现的大体的代码框架,比如Producer-Consumer pattern,比如delegation pattern委托模式。在“放入”元素的方法中,注意notify/notifyAll,在“获取”元素的方法中,注意使用循环而不是if语句,并注意wait的位置。加之理清运行逻辑,让线程能够正确运行。
电梯的架构还是比较清晰的,也比较适合迭代开发。依托于设计模式,电梯运行的逻辑殊途同归。
代码风格不允许成员变量的权限为protected,这直接导致了我的三个具体Scheduler有大量的复用代码(具体体现为写完RandomScheduler后直接ctrl A复制两份,在此之上更改写出另两个Scheduler),仅因为成员变量不能用protected。而Scheduler类中有许多成员变量,若设置为private虽可以用get方法使用变量,一是会让代码变得冗长(1行100字符的直接变为200字符这种),而是也不符合逻辑,这些变量的确应属于Scheduler,可以直接访问。不能用protected功能上不会影响,但实现起来不是很优雅就是了。