面向对象程序设计第二单元总结
面向对象第二单元博客
第五次作业
作业摘要
本次作业的基本目标是模拟单部多线程电梯的运行。
基本思路
作业分析
第一次作业以\(ALS\)调度策略为基准,需要实现单部可捎带电梯,而且分为三种到达模式: Night
, Morning
, Random
, 本次作业只有一部电梯。
架构
第一次作业不是很复杂,架构也很简单:
基本的架构是生产者消费者模式
电梯输入线程作为生产者(productor), 电梯线程作为消费者(Consumer), 而负责沟通二者的桥梁为Table类。
总体上就是输入线程将读入的数据输入到Table类中,而电梯从Table中读取数据。这样可以将所有的数据安全问题交给了Table类,其他的类如果需要修改数据的话只需要直接调用Table中的方法就行。利用生产者/消费者模式可以很干净的解决数据安全问题。
具体实现
线程只用到了\(2\)个,输入线程InputThread, 电梯线程ElevatorThread。
由于有三种到达模式,电梯线程会分三种模式(Night
,Morning
,Random
)运行, 而针对每一种模式,有各自的运行方法:
Night
:乘客在Night 指令到达时一次性全部到达,终点层都是底层。Morning
:乘客到达间隔不超过\(2\)s,起始层都是底层。Random
:允许任何到达情况出现
我们可以发现,Night
和Morning
是比较特殊的两种模式
针对Night:
由于所有请求是一次性输入完成的,所以Night模式是存在最优调度方案的,可以用dp求解。(其他两个模式都是强制在线)
dp的思路也很简单,由于Night模式需要把人从不同的楼层接到\(1\)楼(到达楼层都是\(1\)),而且我们已经知道了所有人的请求,我们首先对所有人按照出发楼层进行排序,然后进行分组(由于电梯容量有限(本次作业为\(6\)),我们每次能够运载的人最大就是\(6\)个)。最后只需要按照组拉人就行。
对于每个人有两个值,\(f_i\)和\(g_i\),其中\(f_i\)代表的是前\(i\)个人需要的最短时间和,\(g_i\)表示人\(i\)所在组第一个人的下标。最后从后往前遍历分组就行。对于每个人\(i\), 我们从\(min(0, i - 6)\)开始遍历,找到位置\(k\in [min(0,i-6),i]\),使得从\(k\)位置到\(i\)位置分成一组所需时间最少。此时总共的时间就是前\(k-1\)个人所需总时间\(f[k-1]\)和最后一组需要的时间和。
可能是假的dp, 不能证明上面的方法就是最优
针对Morning:
Morning模式没有想到什么比较好的策略,最后使用的策略是每次等满6人或者输入结束电梯就走, 到顶层之前边走边放人,最后直接回到\(1\)楼。
测试结果表明等人瞎写策略貌似还行。
针对Random:
Random应该是三种模式中最难的,在搜集资料之后发现有很多中调度方法(除了ALS)之外,如:SCAN, LOOK , SSTF(Shortest Seek Time First)等。但是针对强制在线模式,针对任意某种调度模式,貌似都有样例可以让其调度不是最优,所以最后结论应该是没有针对Random的最优策略,甚至较优策略也不是很好找,因为是强制在线,我们不知道接下来的数据是什么,所以也不可能知道利用什么策略对于这组数据最优,最后用了SSTF算法, 因为重载荷的情况下,最短寻找楼层时间优先算法的平均响应时间较短。
架构分析
第一次的架构还是比较好的,至少后两次作业都是在第一次架构基础上拓展的。
几个比较重要的类都是后续作业的基础:
电梯类,电梯线程类,输入线程类,请求队列类
Metrics
Class | OCavg | OCmax | WMC |
---|---|---|---|
Table | 1.2 | 2.0 | 11.0 |
RequestThread | 2.0 | 3.0 | 4.0 |
Request | 1.0 | 1.0 | 6.0 |
Random | 3.8 | 9.0 | 31.0 |
Pattern | 1.0 | 1.0 | 18.0 |
Night | 4.1 | 12.0 | 29.0 |
Morning | 6.0 | 21.0 | 30.0 |
Main | 1.0 | 1.0 | 1.0 |
ElevatorThread | 2.0 | 5.0 | 8.0 |
Elevator | 1.0 | 1.0 | 19.0 |
可以看出三种模式的确蠢,实现三种模式的过程更多的是面向过程而不是面向对象。
但这次架构的主题并不是三种模式的实现,所以还是有所收获。
这次作业主要学习到的还是消费者/生产者模式
main函数时序图
输入线程时序图
ElevatorThread时序图
BUG分析
自己的BUG
- 在强测中没有被测出bug,但是互测中被发现了一个Bug(?),Morning可能存在轮询的问题,(但是重新交一遍一样的代码就过了???)也可能是评测机波动?
他人的BUG
- 轮询的问题:while(true) 在一些情况下没有进行wait()
第六次作业
作业摘要
本次作业要求模拟多部同型号电梯的运行,并要求能够响应输入数据的请求,动态增加电梯。
基本思路
作业分析
相较与第一次作业,第二次作业最大的不同就是有多部电梯,第一次作业由于只有一个电梯,我并没有用到调度器,而第二次作业就明显需要调度器了。此外第二次作业还有一个不同就是可以动态增加电梯,也就是说请求现在分为两种,一种是乘客的请求,另一种是电梯的请求(临时加电梯)。但是所有的电梯都是同样的电梯。
架构
上一次的很多架构还是可以沿用 : 输入线程, 电梯线程, 电梯类, Table类。
但是由于多部电梯的存在,我们现在需要一个调度器,同时我们发现一个Table好像不够用了,现在我们需要两类Table,一类用来从InputThread接受请求,叫做WaitQueue,另一个直接与每一个电梯相连,叫做小table,这样可以减少我们的工作量,我们需要做的就是将WaitQueue中的请求分发给合适的小table,这样做的巧妙之处就在于我们不需要更改电梯的运行模式,只需要分配请求就行了。
实现
这次的主要任务就是如何调度
相比上次作业,本次作业将Random的电梯运行方式从SSTF变成了LOOK,(SSTF跑强测数据很慢,也可能是我写假了) 其次由于电梯请求的加入,Night不能在所有输入结束之后对乘客请求进行dp,于是采用了贪心策略,就是先接高层楼的人,然后接底层的人,接满为止。
针对Morning
写了一个计算函数,可以计算出每个电梯的运载时间,然后将请求放入时间最短的电梯。
针对Night
将请求放入人数最少的电梯队列
针对Random
按照Random的运行模式,建立了一个计算时间的类,模拟电梯的运行,每次将当前插入的人,电梯状态和小table数据传入,然后找到用时最短的电梯。
架构分析
本次架构根本上还是生产者/消费者模式,与第一次作业不同的是,这次作业有了调度器,也就形成了两层的生产者/消费者模式。第一层的生产者是输入线程,输入线程从标准输入中读取数据,并输入到WaitQueue中,第一层的消费者是调度器线程,调度器线程直接从WaitQueue中获取数据,并且将数据分发给各个电梯的Table。所以调度器也是第二层的生产者,因此电梯线程也就成了第二层的消费者。
类Metrics分析
Class | OCavg | OCmax | WMC |
---|---|---|---|
CalTime | 3.12 | 7 | 25 |
Dispatcher | 1 | 1 | 3 |
Elevator | 1 | 1 | 16 |
ElevatorThread | 1 | 1 | 13 |
InputThread | 3.67 | 5 | 11 |
Main | 5 | 5 | 5 |
MorningDispatcher | 3.88 | 6 | 31 |
MorningThread | 6.75 | 21 | 27 |
NightDispatcher | 3.4 | 6 | 17 |
NightThread | 4.2 | 14 | 21 |
Person | 1 | 1 | 7 |
RandomDispatcher | 3.25 | 6 | 13 |
RandomThread | 3.88 | 9 | 31 |
Table | 1.33 | 2 | 16 |
WaitQueue | 1.4 | 3 | 14 |
各种模式的电梯线程和调度器线程耦合度较高,具体各种模式的实现则更多的是面向过程,但是在现实中可以降低性能而获得更多的拓展性。 但架构上还是有一些拓展性,本次架构基本沿用了MVC结构, Model包含Elevator, Person, Table, WaitQueue, View包含了InputThread, Controller包含Dispatcher和ElevatorThread。
UML类图
BUG分析
自己的BUG
强测和互测没有被发现Bug
他人的BUG
-
调度器调度错误,使得一些人没有分配到电梯线程
-
死锁问题:线程不安全,线程在wait()后不能被notify,从而导致超时问题,但是由于是多线程问题,即便对于相同的数据,也不一定能够复现,而且对于不同的死锁问题,复现的概率也不一样,有些地方10几次就能复现,有些地方需要重复跑一晚上才能复现。
-
有些同学没有在Main函数入口处就初始化输出接口的时间戳
第七次作业
作业摘要
本次作业要求模拟多部不同型号电梯的运行。型号不同,指的是开关门速度,移动速度,限载人数,以及最重要的——可停靠楼层的不同。
基本思路
作业分析
第七次作业需要实现的是多部不同电梯的调度,相较与第二次作业,只有电梯类型发生了变化,对于不同类型的电梯,可以到达的楼层也不一样。因此对于某些情况,乘客可能需要换乘才能达到目的地(但是并没有强制换乘), 由于A电梯是万能电梯。所以即使不换乘也能完成任务?
架构
架构与第六次作业完全一样,仍旧是两层的生产者/消费者模型。而且合并了Random模式和Morning模式, 都采用Look方法运行
型号 | 到达楼层 | 移动速度 | 限乘人数 |
---|---|---|---|
A | 1-20 | 0.6s/层 | 8 |
B | 奇数层 | 0.4s/层 | 6 |
C | 1-3,18-20 | 0.2s/层 | 4 |
而且我们知道C型号电梯跑的最快,同时可到达楼层也最少,其次是B型号电梯,最后是A型号电梯。
所以这次调度的贪心策略就是优先对C型号电梯分配人员,其次对B型号电梯分配人员,然后A型号电梯作为机动电梯,用来作为C或B型号电梯换乘的人搭乘的电梯,但再次之前首先判断B型号电梯能否能搭载C型号电梯换乘乘客,在B型号电梯不能够搭载的情况下使用A电梯搭载。
实现
本次作业的实现基本和上一次相同,再次不过多赘述。
架构分析
Metrics分析
Class | OCavg | OCmax | WMC |
---|---|---|---|
WaitQueue | 1.4 | 3.0 | 14.0 |
Table | 1.3 | 2.0 | 17.0 |
RandomMorningThread | 4.0 | 15.0 | 45.0 |
RandomMorningDispatcher | 3.4 | 10.0 | 38.0 |
Person | 1.0 | 1.0 | 11.0 |
NightThread | 3.6 | 14.0 | 22.0 |
NightDispatcher | 5.3 | 15.0 | 70.0 |
Main | 4.0 | 4.0 | 4.0 |
InputThread | 3.3 | 5.0 | 10.0 |
ElevatorThread | 1.0 | 1.0 | 13.0 |
Elevator | 1.38 | 5.0 | 29.0 |
Dispatcher | 1.0 | 1.0 | 3.0 |
UML类图
BUG分析
自己的BUG
在强测和互测中都没有被发现Bug
他人的BUG
- 死锁:对于只有一条请求的数据,处理完之后程序不能退出
- 送人目的地错误:乘客在换乘后没有再次进入请求队列,最后没有达到目的地
总结分析
线程安全
-
三次作业中,处理数据安全都放在了存放数据的缓存区中(Table和WaitQueue),避免了多个对象同时写入同一数据的问题。
-
为了防止轮询和死锁,在关键地方的同步块中进行了
wait
和notify
操作,三次作业中并没有出现互相调用锁而产生的死锁问题。
BUG发现
- 主要利用评测机,对程序进行了几万组的数据验证,在该过程中发现了自己和他人的bug,但自己的bug都在提交截止前修复完成,(除了第一次对于轮询还不是太懂,而且当时也不会用JProfiler看进程的运行时间,在Moring部分存在着一个小bug),在bug修复过程中有很多奇怪的bug,包含但不限于:电梯上天下地,送人不完整,电梯在两层楼之间反复横跳,由于死锁而不能结束进程\(\ldots\) 其中最难debug的部分就是死锁,对于那种非常容易复现的死锁,我主要原因就是while(true)结构中没有对wait()的情况完全考虑到,并且在相应的地方进行notity。其次用评测机找他人bug过程中,发现他人有死锁的情况,但是对于同一组数据,死锁难以复现(bug藏的比较深,一般的数据都能过,对于特殊数据会卡死,但也不会每次都卡死) ,平均要跑上千组的数据才能复现。
- 手动构造极端数据,
最后发现都不够极端,并没有什么用
第三次作业可拓展性
第三次作业的架构可拓展性比较强,无论是对于增加电梯还是改变电梯类型,或是增加电梯属性等,都只需要对ELevator类中的一些参数更改赋值就行。
心得体会
- 多线程方式的提升
- 这一次的作业让我对于多线程的认识更进一步,特别是在解决线程安全问题(第二次作业de了一天bug才解决)和保护数据安全方面。
- 架构方面的提升
- 对生产者/消费者模型和MVC模型都有了一定的实践。
- 对工程方法有了更多的了解,三次作业的架构都很明确,相较与第一单元的作业,写代码的过程很顺畅
- 对多线程单元的体会:分三种模式感觉不是很有必要,这样反而使得编程更面向对象,虽然对于特殊情况的效率提升了,但是降低了内聚度和可拓展性。