BUAA OO BLOG UNIT2

BUAA OO UNIT2 BLOG

摘要:

Narrowly escape from Elevator Unit......

度过愉快的电梯单元,对多线程的设计有了很深的体会。多线程的设计,主要还是聚焦于多线程设计模式的使用、线程安全、轮询(wait-notify机制)几个方面(电梯调度策略像是买一送一<( ̄▽ ̄)/)

以下针对线程安全、调度器、架构设计、复杂度分析、bug分析、hack策略、预防轮询和心得体会几个方面,做出第二单元总结。

 

线程安全

线程安全主要集中在线程之间的交互,三次作业,我都选择了使用生产者-消费者模式

生产者消费者缓冲区
InputThread Schedule WaitQueue、RequestQueueBuffer、ElevatorBuffer
Schedule Elevator RequestQueue

第五次作业:

在缓冲区类中全部使用了synchronize关键字完成共享数据保护。

 

第六次作业:

由于电梯数量可以增加,同时Synchronized 使用锁少量的代码同步问题,Lock适合锁大量的同步代码。

所以第六次作业的电梯的缓冲区RequestQueue使用了lock进行数据同步,其余的缓冲区依然是使用了synchronize关键字。

 

第七次作业:

延续第六次作业的设计,只不过是将ReentrantLock锁换成了ReentrantReadWriteLock第六次作业就可以用,但是没有想清楚读写锁的await-signal机制)。考虑到电梯很多时候会去读取等待队列的状态,读写互斥有助于提升性能。从强测性能分的角度,两次作业都采用自由竞争,但是第七次作业的性能分显著高于第六次✌️

 

调度器

在我的架构中,有且只有一个总调度器

每一种类型(即同楼层的环形电梯或同座的纵向电梯为同一类型)的电梯共同拥有一个等待队列。Schedule是一个线程,负责将总等待队列中的请求分配到每种电梯的队列中。

Schedule线程会和Elevator和InputThread两类线程交互。线程交互使用了生产者、消费者设计模式。

对于InputThread, WaitQueue和Schedule,InputThread是生产者,Schedule是消费者。

对于Schedule, RequestQueue和Elevator,Schedule是生产者,Elevator是消费者。

 

第五次、第六次作业:

调度器逐个获取总等待队列中的请求,并依据请求,将其分配到对应的电梯等待队列中。

 

第七次作业:

相较于前两次作业,第七次作业的Schedule增加了路径规划的功能,并依据规划的路径,将请求分配到对应的电梯队列。

 

电梯策略:

第五次作业:

只有纵向电梯,采用了look算法。

 

第六次作业:

纵向电梯采用look算法,横向电梯采用ALS策略。电梯之间采用自由竞争。

 

第七次作业:

整体和第六次作业类似,添加了ALS的基准换乘策略。

 

架构设计

线程安全和电梯调度策略都已在前文中提及了。架构中还需要特别说明的是Passenger类和电梯类。

纵向电梯和横向电梯实现了一个电梯接口,这样可以放在同一个容器中更方便地被调用。

第七次作业中,新写了一个Passenger类,继承官方包中的PersonRequest类。采用流水线设计模式,给Passenger添加了一个BitSet类型的属性。我的换乘策略是ALS即乘客最多三次进出电梯,所以BitSet为三位,第0位为在出发座的纵向电梯移动,第1位为在环形电梯中的移动,第2位为在目标楼座纵向电梯中的运动。三个步骤中,出发地和目的地都在变化,所以设置了相应的set方法,并重写了get方法。乘客每次出电梯时,会根据其BitSet属性,判断是否到达目的地。

UML图如下(类中仅保留重要的属性和方法)

 

 

时序图:

                                                                                   

 

复杂度分析

类复杂度:

ClassOCavgOCmaxWMC
BuildingElevator 1.93 7.0 29.0
Counter 1.0 1.0 4.0
ElevatorBuffer 1.0 1.0 2.0
FloorElevator 2.65 11.0 45.0
InputThread 4.5 8.0 9.0
MainClass 2.0 2.0 2.0
Passenger 1.0 1.0 10.0
Printer 1.0 1.0 1.0
RequestQueue 2.55 6.0 28.0
RequestQueueBuffer 1.0 1.0 4.0
Schedule 4.6 11.0 23.0
WaitQueue 1.4 3.0 7.0
Total     166.0
Average 2.10 4.15 12.77

由上述图表可以发现,输入线程类和调度器类类复杂度较高,主要原因是使用了很多if-else语句去进行请求类型和换乘策略的判断。

 

主要方法复杂度:

方法CogCev(G)iv(G)v(G)
FloorElevator.run() 32.0 6.0 9.0 15.0
Schedule.routePlan(Passenger) 21.0 7.0 13.0 15.0
InputThread.run() 19.0 3.0 9.0 9.0
BuildingElevator.run() 17.0 6.0 8.0 11.0
RequestQueue.getDirection(Passenger) 14.0 4.0 2.0 6.0
Schedule.run() 13.0 5.0 9.0 10.0
BuildingElevator.passengerOut() 11.0 1.0 5.0 5.0
FloorElevator.passengerOut() 11.0 1.0 5.0 5.0
RequestQueue.getAllPassengers(ArrayList, ArrayList, ArrayList, int, Elevator) 11.0 3.0 8.0 9.0
Schedule.initiatePassenger(Passenger) 7.0 1.0 5.0 6.0
FloorElevator.getDirection(Passenger) 6.0 2.0 1.0 4.0
Total 209.0 114.0 177.0 205.0
Average 2.64 1.44 2.24 2.59

表格中罗列了主要方法的复杂度,可以发现,一般线程的run方法的复杂度较高,尤其是两个电梯类,主要还是因为我没有设置专门的策略类去完成电梯调度。

 

bug分析

第五次作业:

RTLE: 超时的原因是对指导书的理解有误,误以为一个电梯的行为需要连续输出,所以把开门、下人、上人、关门这四个输出放在了同一个synchroize(TimableOutput.Class)保护的代码块中。

 

第六次作业:

RTLE: 超时的原因是环形电梯的调度出现了问题。之前环形电梯采用的是look算法,但是出现如下数据,电梯就会一直arrive不会接人。

[0.0]ADD-floor-7-2 
[1.5]1-FROM-A-2-TO-B-2
[1.5]2-FROM-B-2-TO-A-2
[1.5]3-FROM-C-2-TO-B-2
[1.5]4-FROM-D-2-TO-C-2
[1.5]5-FROM-E-2-TO-D-2

 

第七次作业:

RTLE:没有处理好if-else if 的分支判断,结构如下,即无论是否符合条件都会执行remove语句。

if(...) {
  ...
} else if(...) {
...  
}
remove()

 

hack策略

第二单元几乎不会出现WA的情况,绝大部分为RTLE和CTLE,所以主要针对大数据、电梯策略两方向进行检测。

  • RTLE。在互测数据输入截至时间前,一次性投喂最大数量限制的乘客请求。

  • 轮询检测。投喂一些数据后,等待很长一段时间后再次投喂。

  • 线程终止。如,以一条增加电梯的请求去结束投喂,试试线程能够结束。

  • 环形电梯的look检测。使用bug修复中提及的环形look算法出现的问题。

 

预防轮询!!!

一开始还不知道轮询是什么。。。。。。直到亲身试错(。_。)

用我现在的理解,轮询就是,队列中没有请求,线程就应该交出锁和CPU的控制权进入等待区,如果没有wait,线程的run方法就会一直向队列发出询问,一直占用CPU

在我的架构中,主要有三处地方需要预防轮询。

  1. 调度器从等待队列获取请求。如果等待队列为空,调度器应该wait,有新的请求再notify。

  2. 电梯从队列中获取请求。如果队列为空且电梯中无乘客,电梯应该wait,队列中有新的请求再notify。

  3. 第七次作业,调度器查看计数器,计数器不为空且等待队列为空且获取到输入结束时,调度器也应该wait,直到计数器的值发生变化再唤醒。

    需要特别注意的是,第七次作业的电梯线程容易出现轮询。原因是队列中有人,但是该乘客的楼层被阻塞不可达,所以判断队列为“空”的方法需要重写。

 

心得体会

1.Multi-thread = Metaphysics ┑( ̄Д  ̄)┍。

  • BUG十分难找

  • 本地测试是正确的,可能到测评机上就会出问题。

  • 同样的代码版本,每次提交,测评机报错的测试点可能不一样。

  • 互测数据,可能需要几次提交才能把bug hack出来。

多线程bug会迟到,但是从不会缺席💔

 

2.实际编程时,多线程的设计和单线程的设计存在着一些差异。在考虑类的协同的时候,需要更多的思考线程安全和轮询的问题。

 

3.虽然多线程bug频出,但是在架构迭代方面还是有所进步的。在第五次作业奠定了一个比较好的架构之后,第六次、第七次作业基本上都是在第五次作业的基础上添加功能。第六次作业添加了一个环形电梯类和ElevatorBuffer,第七次作业添加了一个Passenger类和计数器。良好的高内聚低耦合,极大的减少了每次作业的工作量。(上次blog立下的flag做到了🤟

 

 

 

posted @ 2022-04-30 14:35  WIT23  阅读(4)  评论(0编辑  收藏  举报