面向对象程序设计第二单元总结

面向对象第二单元博客

第五次作业

作业摘要

本次作业的基本目标是模拟单部多线程电梯的运行。

基本思路

作业分析

第一次作业以\(ALS\)调度策略为基准,需要实现单部可捎带电梯,而且分为三种到达模式: Night, Morning, Random, 本次作业只有一部电梯。

架构

第一次作业不是很复杂,架构也很简单:

基本的架构是生产者消费者模式

电梯输入线程作为生产者(productor), 电梯线程作为消费者(Consumer), 而负责沟通二者的桥梁为Table类。

总体上就是输入线程将读入的数据输入到Table类中,而电梯从Table中读取数据。这样可以将所有的数据安全问题交给了Table类,其他的类如果需要修改数据的话只需要直接调用Table中的方法就行。利用生产者/消费者模式可以很干净的解决数据安全问题。

具体实现

线程只用到了\(2\)个,输入线程InputThread, 电梯线程ElevatorThread。

由于有三种到达模式,电梯线程会分三种模式(Night,Morning,Random)运行, 而针对每一种模式,有各自的运行方法:

  • Night:乘客在Night 指令到达时一次性全部到达,终点层都是底层。
  • Morning:乘客到达间隔不超过\(2\)s,起始层都是底层。
  • Random:允许任何到达情况出现

我们可以发现,NightMorning是比较特殊的两种模式

针对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),避免了多个对象同时写入同一数据的问题。

  • 为了防止轮询和死锁,在关键地方的同步块中进行了waitnotify操作,三次作业中并没有出现互相调用锁而产生的死锁问题。

BUG发现

  • 主要利用评测机,对程序进行了几万组的数据验证,在该过程中发现了自己和他人的bug,但自己的bug都在提交截止前修复完成,(除了第一次对于轮询还不是太懂,而且当时也不会用JProfiler看进程的运行时间,在Moring部分存在着一个小bug),在bug修复过程中有很多奇怪的bug,包含但不限于:电梯上天下地,送人不完整,电梯在两层楼之间反复横跳,由于死锁而不能结束进程\(\ldots\) 其中最难debug的部分就是死锁,对于那种非常容易复现的死锁,我主要原因就是while(true)结构中没有对wait()的情况完全考虑到,并且在相应的地方进行notity。其次用评测机找他人bug过程中,发现他人有死锁的情况,但是对于同一组数据,死锁难以复现(bug藏的比较深,一般的数据都能过,对于特殊数据会卡死,但也不会每次都卡死) ,平均要跑上千组的数据才能复现。
  • 手动构造极端数据,最后发现都不够极端,并没有什么用

第三次作业可拓展性

第三次作业的架构可拓展性比较强,无论是对于增加电梯还是改变电梯类型,或是增加电梯属性等,都只需要对ELevator类中的一些参数更改赋值就行。

心得体会

  • 多线程方式的提升
    • 这一次的作业让我对于多线程的认识更进一步,特别是在解决线程安全问题(第二次作业de了一天bug才解决)和保护数据安全方面。
  • 架构方面的提升
    • 对生产者/消费者模型和MVC模型都有了一定的实践。
    • 对工程方法有了更多的了解,三次作业的架构都很明确,相较与第一单元的作业,写代码的过程很顺畅
  • 对多线程单元的体会:分三种模式感觉不是很有必要,这样反而使得编程更面向对象,虽然对于特殊情况的效率提升了,但是降低了内聚度和可拓展性。
posted @ 2021-04-23 16:10  QuantumBolt  阅读(97)  评论(1编辑  收藏  举报