BUAA-OO Unit 2 总结

文章详细描述了一个多线程电梯系统的设计过程,包括架构迭代、生产者-消费者模式的应用、同步机制、分配器和调度器的实现,以及如何处理双轿厢电梯带来的同步冲突。作者强调了线程安全、层次化设计和子类继承在代码复用中的重要性。
摘要由CSDN通过智能技术生成

0 前言

在 Unit2 中,最终要求实现模拟一个多线程实时电梯系统:有多部电梯,电梯可以在楼座内1-11层之间运行。系统从标准输入中读入乘客请求信息(起点层,终点楼层),根据此时电梯运行情况(电梯所在楼层,运行方向等)将乘客请求合理分配给某部电梯,然后被分配请求的电梯会经过上下行,开关门,乘客进入/离开电梯等动作将乘客从起点层运送到终点层。


1 架构与迭代

1.1 整体架构与设计模式

除了迭代分析外,本文中的所有设计架构均指第三次作业最后的架构。如无特殊说明,本文的”请求“均指乘客的请求。将业务逻辑划分为读取请求->分配请求->处理请求三个阶段,采用生产者-消费者模式,以请求池为托盘,输入线程、电梯线程、分配线程互为生产者和消费者,协作完成请求,类的设计如下:

  • MainClass: 程序执行入口,创建并初始化请求池、分配器、调度器、电梯、全局监控器等业务对象,创建输入线程、分配线程、电梯线程并启动。
  • InputThread: 输入处理线程,解析输入,设置电梯重置请求,往请求池中添加人需求
  • Monitor:全局监控器,监控人请求完成情况以结束输入,监控重置请求完成情况以锁住分配器
  • RequestTable:人请求池,分配器的缓存队列
  • AllocatorThread:分配线程,调用分配器处理请求池中的请求
  • Allocator: 分配器,根据电梯状态采用影子电梯策略分配请求
  • ElevatorThread: 电梯线程
  • ActionType: 电梯行为类型
  • Elevator:电梯类,封装数据和基本行为(单轿厢)
  • DoubleCarElevator: 双轿厢电梯类,继承电梯类,改写临界层
  • Scheduler: 调度器(单轿厢),
  • DoubleCarElevator: 双轿厢调度器,继承调度器

1.2 三次作业中的设计迭代

本单元作业未经历大幅度重构,迭代开发循序渐进,体验良好。

1.2.1 第一次

第一次作业的请求指定了特定的电梯,所以无需分配器进行分配,笔者也采用了不同于后续的架构,各个电梯线程的调度器直接从请求池中查询自己的请求,电梯本身不带有待处理请求队列,而是直接与总请求池交互。输入线程,电梯线程协作完成 读取请求->处理请求,完成了电梯的基本功能和整体架构。

UML类图如下:
在这里插入图片描述

1.2.2 第二次

第二次作业的请求不再指定了特定的电梯,并且新增了重置请求,因此增加了分配线程和分配器来分配请求,各个电梯线程增加待处理请求队列来存储各自请求,相当于各自有自己小的请求池,调度器只需要将查询对象有总请求池变为待处理队列即可。为了更好的进行线程协作,还增加了全局监控器

UML类图如下:
在这里插入图片描述

1.2.3 第三次

第三次作业新增了双轿厢电梯这一类型,采用了子类继承了方法新增了双轿厢电梯类和相应的调度器类别以实现功能,其余部分无较大改动。

UML类图如下:
在这里插入图片描述

1.2.4 未来扩展能力

目前实现的电梯系统能处理的请求是在捎带前就得知目的楼层的,这与现实情况不符,若更改功能为仅知道出发楼层,捎带乘客后才得知目的楼层,那么只需要更改调度器的调度策略、分配器策略以及存储请求类的形式,各个线程和电梯本身的功能不需改变,扩展较为方便。

1.3 线程协作

1.3.1 UML协作图

Actor为Main主线程。

输入线程负责读取请求放入请求池和设置重置请求:

在这里插入图片描述

分配线程负责分配请求池中的请求:

在这里插入图片描述

电梯线程负责处理电梯的运行和以及请求池放回请求:
在这里插入图片描述

1.3.2 全局监控器 Monitor

全局监控器相当于一个计数同步锁来控制电梯的重置和结束行为。

monitor对象会监控当前由多少个重置请求正在进行中(输入线程接受增加,电梯线程完成减少),以此锁住分配器避免极端情况下单个电梯的高负荷。 同时,monitor对象还会监控当前完成了多少个请求(输入线程接受增加,电梯线程完成减少),以此判断是否结束输入线程进而结束其他线程。

1.3.1 伙伴电梯 buddyElevator

对于第三次作业中新增的双轿厢电梯,笔者的设计逻辑是将同一井道的两个电梯互相称作伙伴电梯1-A1-B的伙伴电梯,1-B1-A的伙伴电梯)。双轿厢电梯拥有其伙伴电梯的直接引用以完成协作。

引入了双轿厢电梯后,单个电梯可能无法完成请求,由此需要设计换乘策略。通过分析不难得知,一对伙伴电梯是可以协作完成任意请求,由此笔者采取了单个普通电梯或一对伙伴电梯承包一个请求的方法。具体而言就是对于任意请求,若请求跨越了换乘楼层:(toFloor - transFloor) * (fromFloor - transFloor) < 0,则将请求从换乘楼层分为两段,依照分区将第一段请求分配给电梯并将第二段请求用容器存储,当完成请求时查询容器若还有下一段则将请求分配给伙伴电梯。

1.4 变与不变

由上述分析可知,线程类以及电梯自身的行为在迭代中较为稳定,而分配策略和调度策略随着功能要求的迭代也发生了较大的更改。可见,通过提高内聚和解耦功能可以使得我们在迭代开发中只需要修改特定的功能部分,便于开发维护。

1.5 度量分析

指标含义见第一次总结

在这里插入图片描述
在这里插入图片描述

如图所示,电梯类由于参杂了部分判断的调度策略使得其功能复杂,使用了过多的代码,可以在interact的交互和双轿厢的move抽象出共同的逻辑来进一步优化。同时,分配器策略、调度器策略以及电梯线程使用了较多的分支判断也带来了一定的复杂度,可以提炼有共性的分支来增加层次进行优化。

2 同步块与锁

同步块与锁用于控制对线程共享对象的访问,在本次作业中主要体现在电梯对象、请求池对象上,全局监控器双轿厢控制在本文中也有讨论。

2.1 电梯

电梯线程操纵电梯运行,同时分配线程会给电梯新增请求,输入线程会给电梯设置重置请求。由此笔者以电梯为同步控制对象为电梯的接受请求方法,遍历请求队列的捎带方法,设置重置方法加上了同步锁。

2.2 请求池

电梯线程在电梯重置时往请求池放回请求,同时分配线程从请求池中取出请求,输入线程往请求池中放入请求。由此笔者以请求池为同步控制对象为请求池的放入请求方法、取出请求方法加上了同步锁。

3 分配器与调度器设计

为了区分概念,参考上述架构,笔者在本文中将基于电梯等待请求队列负责电梯运行逻辑的功能模块称为调度器,将基于请求池和特定逻辑分派请求到各电梯请求队列的功能模块称为分配器

3.1 分配器

3.1.1 电梯管理

分配器用ArrayList容器直接管理电梯对象,为了管理双轿厢电梯,笔者采用了成对存储的思想,编号为i的普通电梯存储在第2*(i-1)个索引处, 编号为i-Ai-B的双轿厢电梯分别存储在第2*(i-1) 个和第2*(i-1)+1个索引处。

3.1.2 重置请求

由于重置请求指定了特定的电梯,具有较高的优先级而不涉及执行分配的线程的等待行为,所以在输入线程读取到重置请求时,直接调用分配器的重置分配方法,将重置请求置于电梯的待处理重置请求中。如果是双轿厢类型的重置,则将调用原电梯的toDCElevator()方法生成双轿厢电梯并在容器中替换原电梯,同时创建各自的调度器与相应的电梯线程并启动电梯线程。

3.1.3 普通请求

普通请求由输入线程放入请求池中,分配线程不断的从请求池中取出请求并调用分配器分配请求。笔者采用了影子电梯的分配方法,即在分配请求时刻创建各个电梯的拥有相同状态的影子,将请求“分配“给各个影子电梯并依照相同的调度策略模拟系统运行,根据模拟的运行时间shadowTime和耗电量consumption的加权性能指标,判断局部最优性能并分配给实际的电梯。实现上述功能只需要深克隆电梯,并将运行过程中会对系统有实际影响方法改写,如sleep变为shadowTime的增加,无效化输出行为和有关请求池、全局监控器的行为,其余调用原有电梯和调度器逻辑即可。

3.2 调度器

调度器采用了Look策略,首先判断是否需要重置和结束;然后判断是否需要与当前楼层交互(乘客进出),捎带同方向的请求;接着判断是否移动:载有乘客,前方有请求;最后判断是否转向:后方有请求,以及无事可干的等待行为。判断策略具有明显的优先级,只有在高优先级行为都不成立的情况下才会执行低优先级行为,避免了策略冲突。

4 双轿厢与临界区冲突

在第三次作业中,新增了双轿厢电梯的功能需求,也因此带来了相应的同步互斥问题。

4.1 临界资源分析

双轿厢电梯是指在同一电梯井道内同时拥有两个独立的电梯轿厢,而电梯系统默认的普通电梯是指在一个电梯井道内只有一个轿厢。为了保证两个轿厢不相互碰撞,将楼层分为上区、下区、换乘楼层,其中上区为换乘楼层以上的所有楼层,下区为换乘楼层以下的楼层,均不包含换乘楼层。在整个运行过程中,要求轿厢 A 只能在下区和换乘楼层运行,轿厢 B 只能在上区和换乘楼层运行,同一井道内的两轿厢不能同时位于换乘楼层。”

一个请求很有可能是需要跨越换乘楼层的,这就要求我们在设计时需要避免两个轿厢相撞,让它们以一定的向后词语依次到达/离开换乘楼层来达到对换乘楼层处捎带请求的访问与处理。

4.2 解决方案

面对此类临界区资源冲突的问题,一个自然的想法是使用同步互斥锁来对临界区的访问进行控制,基于将两个双轿厢电梯绑定在一起的调度策略,笔者在创建双轿厢对象时每一对双轿厢电梯各自占有自己的dcLock同步锁对象, 同时用occupy = (curFloor == transFloor)的属性值表示对临界区资源的占有。接下来就是如何处理同步锁和访问行为的逻辑。

只有在同时往换乘楼层移动时才会造成冲突,电梯的其他行为不受印象,所以笔者改写了双轿厢电梯的移动方法move(),首先通过当前所在楼层、电梯移动方向、电梯类型来判断目的地是否换乘楼层,raise属性表示访问临界区资源的请求:

boolean raiseA = (carType == CarType.A) && (getUpward())
                && (getCurFloor() == transferFloor - 1);
raise = raiseA | raiseB;

若请求为真则查询伙伴电梯occupy属性,如果其不处于占用状态则直接调用父类super.move()方法进入换成楼层并更新occupy(true)raise(false)属性, 否则将伙伴电梯唤醒,促使其离开换乘楼层且自身陷入基于dcLock的等待。接下来只需要配合相应的临界区资源释放逻辑就大功告成了。

基于临界区冲突行为,双轿厢电梯可以不捎带请求地离开换乘楼层,因此为其编写了主动放弃方法given()根据电梯类型设置方向,移动释放临界区资源,更新occupy(false)并唤醒dcLock上等待的伙伴电梯,同时改写调度器方法:

 if (actionType == ActionType.WAIT && buddyElevator.getRaise() && elevator.getOccupy()) {
        return ActionType.GIVEN;
 }

当电梯不执行其他行为且伙伴电梯申请自己占用着的临界资源时,主动离开释放资源。此外,在电梯原先的移动方法中,如果是从换乘楼层离开则相当于被动释放了资源,也需要及时更新状态并唤醒伙伴。

int prevFloor = getCurFloor();
super.move();
if (prevFloor == transferFloor) {
   occupy = false;
   dcLock.notifyAll();
}

4.3 评价

上述解决方案通过同步锁的控制可以有效避免两个轿厢相撞。同时在一个轿厢离开换成楼层放弃临界区资源时,其会通过该同步锁及时通知另一双轿厢电梯,减少了无意义的等待,提高系统性能。

但是该解决方案中将部分控制逻辑封装在了电梯自身的移动行为方法中,有违功能分离的设计初衷,可以将该部分逻辑提取到调度器的判断逻辑中来进一步的改进。

5 bug & debug

第一次作业中未出现bug。

第二次作业中出现了请求池死锁的bug。原因是把重置请求放进请求池由分配器线程完成,电梯线程在执行重置请求时会将承载请求退回请求池,等待请求池的锁,而分配线程在分配请求时会占用请求池,若恰好分配给该电梯(高并发下是必然)则会等待该电梯完成重置请求,造成死锁。解决时将重置请求调至输入线程处理,同时用全局监控锁防止单个电梯接受过多请求造成超时的情况。

第三次作业中出现了线程冲突的bug。原因是在电梯线程的调度器遍历查询电梯的请求队列时没有用同步锁控制,在遍历过程中如果分配线程分配了新的请求则会修改该请求队列,造成线程冲突前两次锁了但是在第三次简化过多的无用锁时没有思考清楚而去除了必要的同步锁。解决办法是在遍历外层加上锁控制,保证线程安全。

debug方面,由于多线程时间相关的特性,以往的行断点方法在本次作业中无法使用,除了简单的特定位置输出变量法,本次还学习到了如何使用jconsole来对各个线程的死锁/死循环/轮休/长眠等情况进行定位查询,此外还可以查看cpu运行时间和内存占用情况,是个功能强大的工具。

同时,通过构造高并发的极端数据也在互测中多次hack到别人。学习到了对系统进行压力测试的方法,更好的发现系统的隐藏漏洞提高鲁棒性。

6 心得体会

在本次作业中,接触和学习了多线程程序的特点和设计方法,收获颇丰。

6.1 线程安全

多线程会同时对临界区发起访问,从而带来并发问题,而同步锁机制是处理并发问题的一个有效手段。最主要的是辨析出什么对象才是会引发线程冲突的临界区资源,并配置相应的同步锁。同步锁应该尽可能的在对象内部设置而非由调用者在外层设置,如此符合内聚原则且不易遗漏冲突情况。此外,只对必要的临界区资源上锁,同步锁在保障线程安全的同时会造成一定的性能损失,且不经过设计思考的过多同步锁会引发死锁问题。

6.2 层次化设计

本单元是层次化设计比较明显的一个单元,体现在类似流水线的架构上,每个类只负责自己部分职能,如电梯只负责运行行为,调度策略由抽象的调度器负责,输入线程只负责往请求池放入请求,而具体分配由后续的分配线程调用分配器完成。这要求我们将业务逻辑划分成不同的阶段,为每个子任务设计相应的功能,从而解构复杂性。

同时,在新增双轿厢电梯时,也学习到了子类继承的用法,通过大部分继承父类属性方法和重写个别功能方法,实现了代码功能的重用,极大提高了代码的编写简易度和可维护性。

  • 30
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值