OO第二单元多线程电梯总结
写在前面
本单元我们首次接触了多线程编程,相较于我们之前写的单线程程序,多线程考虑的重点不尽相同。多线程最需要关注的是线程间通讯,
而不仅仅是一个线程内的逻辑。而如何安全地处理好共享对象,完成线程间的安全交互,就是我们面临的最大挑战。
S1设计策略
本单元的三次作业,我采用的是一般的生产者—消费者模式,创建了Tray类来作为共享对象,每个电梯是一个消费者线程,输入单独开
出一个生产者线程。我的设计思路是将所有的输入请求均保存在tray对象中,电梯只负责上下楼,内部不保留任何数据(也就是说我的
电梯内并没有去维护一个请求队列),是否进人、上楼或下楼或静止,都需要访问tray后才能得到结果。我最初这样做的目的是想让tray
成为“全知者”,甚至让tray来计算最佳分配策略(但事实证明这样效果未必好,这是后话)。
第一次作业由于是容量无限单电梯,并不太涉及多个线程之间的协调,我也并未出现什么bug,主要的时间花在调度策略的思考上。我
使用的调度策略是自己思考出的向量法,是可证明的局部最优调度策略,依靠这个最优策略我拿到了99.97分(由于输入时间的不确定性
,并不能保证所有点都是满分),虽然第一次作业并不困难,这个分数也还算比较高了。
第二次作业由容量无限单电梯变成了定容多电梯,我将共享对象tray作为调度器,在请求进入tray的同时,根据tray当前掌握的信息,为
请求指派服务电梯(即只有对应的指派电梯才会考虑该请求,其它电梯完全忽略之)。由于tray持有了所有电梯的运行状态,对tray的访
问与修改变得极其频繁,导致我不得不为tray设置了较多的同步方法,于是在自己测试的时候本单元第一次死锁产生了。在设计上我犯
的最大的错误就是给共享对象tray添加了太多的功能与信息,违反了SOLID原则,并且耦合度太高。但由于时间关系,我并未在第二次作
页将该错误修复,不过强测与互测中并没有发现死锁的问题。
虽然第二次作业没有出问题,但我还是在第三次作业调整了架构。tray方法将功能下放,由电梯自行选择上下楼及进出人,采取随机上
人的策略不再统一调配,由于采取了这样的策略,而第三次作业又多出了换乘的问题,我在处理请求的时候将其预拆分,即在请求内加
入换乘楼层的信息。这样一来,减少了电梯线程之间的通信,也减少了电梯对tray的修改,更加符合了SOLID原则中的SRP原则。至于调
度问题上,随机抢人(无调度)的策略甚至优于调度器统一分配,这是我没有预料到的,不过从分数上看确实如此,第三次较第二次分
数有所提高,也达到了99.92。
S2可扩展性分析
单一职责原则(Single Responsibility Principle)
一个类应该只有一个发生变化的原因:
事实上,正是为了适应SRP原则,我在第三次作业中调整了架构,原本的elevator类在许多情况下都可以提出修改seek请求类,我将其
改成了只有结束所有换乘后elevator类能提出对seek的的修改,减少了对seek类的操作。代码整体上遵守了SRP原则。
开闭原则(Open Closed Principle)
一个软件实体,如类、模块和函数应该对扩展开放,对修改关闭:
这一点我做的还算可以。三次作业间的迭代,除开第三次使用的新的调度算法而改变了电梯类的changeStatus之外,基本都是在原有的
base类的基础上进行继承,扩展起来也十分方便
里氏替换原则(Liskov Substitution Principle)
所有引用基类的地方必须能透明地使用其子类的对象:
我的代码只出现了elevator类对base类的继承,所以并没有什么不能透明使用子类的地方。
迪米特法则(Law of Demeter)
如果两个软件实体无须直接通信,那么就不应当发生直接的相互调用,可以通过第三方转发该调用:
电梯间通讯正是通过调用tray的方法来进行的,这一点上我做的应该还行。
接口隔离原则(Interface Segregation Principle)
1、客户端不应该依赖它不需要的接口。
2、类间的依赖关系应该建立在最小的接口上:
呃,我少有用到接口,在这一点上可能做的不太好。
依赖倒置原则(Dependence Inversion Principle)
1、上层模块不应该依赖底层模块,它们都应该依赖于抽象。
2、抽象不应该依赖于细节,细节应该依赖于抽象。:
本次涉及到的继承较少,层次也较浅,并没有太多地方能够体现这一点。。。
S3基于度量的程序结构分析
由于三次作业继承性十分明显,而又以第三次作业功能最为复杂,所以这里只对第三次作业进行分析。
·方法复杂度
涉及到的方法较多,我这里只对复杂度较高的方法及其成因进行分析。
可以看到,tray的addseek和outIsEmpty方法复杂度飙红,这里实属无奈,因为这两个方法内有着大量的逻辑分支,尤其是outIsEmpty
方法,对三类电梯都有不同的分支。不过话说回来,我确实应该将outIsEmpty改为elevator类的一个方法,这样可以减少代码的复用率。
其次是Base类的run方法,这里是由于根据不同信号电梯需要维持的状态比较多,并没有好的改进手段。
最后是Elevator类的solve方法,这确实是代码中逻辑最复杂的一块,它负责是否开关门/上下人,我认为此处复杂度略高一点无伤大雅。
·类复杂度
确实,Tray类和Base类的复杂度太高了,我并没有为Tray和Base类建立一个好的“中枢处理”类成员,而是全部由主类自己的各类方法
完成了任务的处理与分配,导致这两个类的复杂度颇高。好的处理手段应该是在这两个类内部建立专门的功能对象,而不是全由顶层的
方法调用来处理。
·类图
第三次作业中,我的电梯分A,B,C三类,均继承自一个base基类,通过一个共享对象tray(同时也是分派中心)来实现请求的中
转安排,InputPanel负责addrequest,由各个elevator来checkrequest后决定是否开门以及行动策略。
·UML协作图
S4自身程序的bug分析
事实上我只在第三次互测中出现了一个bug,是由于我将A类电梯的人数上限也误设置为7导致的(不过强测居然侥幸没有测出来),这
说明认真读题确实很重要。。。
S5发现他人bug的策略
由于本次是多线程程序设计,我在hack他人代码时主要是在线程安全上进行考虑。重点关注对方的同步方法,加锁顺序,看是否可能引
起死锁或线程间交互问题。还需要注意考察对方的共享对象是否每一个操作都线程安全。bug基本都出现在以上两处,不过很可惜的是我
根据程序构造出的样例很难hack中对方,这也是由于多线程的不稳定性导致的,一些bug在我本地很容易跑出来,但交到互测平台就不行,
我也很无奈。。。
S6自身心得体会
虽然是第一次接触多线程,但总的来说三次作业完成的还是比较轻松,相较于第一次的简单多项式求导,我已经初步掌握了面向对象编程
的一些技巧与要点。在线程安全方面,基本上考察的就是灵活使用wait和notifyAll以及正确地选择同步代码块的范围,在这一方面我理解
还算可以;在设计原则上,我一直在考虑和关注SOLID原则,我确实感受到这让我的代码可读性与容错率大大提高。通过这一单元的学习
任务,我能感受到我在面向对象编程的道路上又进了一小步。