2024BUAA面向对象UNIT2总结

 第二单元依托电梯系统这个背景,逐步掌握面向对象中的多线程程序设计。多线程来自对客观现实世界中多事物协作机制的模拟,典型的设计模式是生成者-消费者模式,这一模式也贯穿了三次作业始终。多线程程序设计中一个必须解决的问题就是各个线程之间的同步互斥问题,或者是线程安全问题,这是多线程程序正确性的重要保障。

接下来将从同步块的设置与的选择(线程安全相关)、调度器设计(调度策略相关)等角度依次分析三次作业中各个类、各个线程之间的协作关系,并给出相应的UML类图。并以工程迭代的视角考察每次实现中稳定的内容和易变的内容(包含在迭代思考中)、功能与性能的协调,同时对程序中出现过的bug作简要分析。

HW5

架构分析

hw5要求实现一个基本的多线程电梯实时系统。通过对问题的抽象,并结合生产者-消费者的基本模式,可得出在这个系统中存在输入线程、调度器线程、电梯线程以及请求池(共享对象),他们之间的协作关系可由下图简要表示。

输入线程获得请求,将其放入请求池(大盘子)中,由调度器线程从请求池中取出请求,按照一定的调度规则分配到6个电梯各自的请求池(小盘子)中的一个,之后由电梯线程从自己的小盘子中按照一定的捎带策略取出请求放入自己的内部队列,最终完成一个乘客从某一楼到另一层楼的请求。整体架构符合标准的生产者-消费者模式,且存在多级生产者和消费者:输入线程是大盘子的生产者,而调度器是消费者;同时调度器也是每个电梯小盘子的生产者,对应的消费者成为了相应的电梯线程。当然由于本次作业乘客指定乘坐哪个电梯,所以调度器的实现逻辑并不复杂,只需把这个乘客加入到他指定电梯的小盘子中即可。

调度策略

本次作业只需考虑电梯的调度,即电梯的运行状态如何确定的问题。那么,电梯的运行状态由什么描述呢?结合有限状态机的观点,认为电梯每到达一层即为电梯的一个确定的状态,所以电梯的状态改变可以由电梯的运行方向唯一确定。电梯上行,下一个状态电梯的楼层增加1;电梯下行,下一个状态电梯的楼层减少1;电梯不动,下一个状态电梯仍在原楼层。

由于调度器的存在,6个电梯线程之间不存在交互,而是由调度器负责与6个电梯线程交互,所以一个电梯在某一时刻的下一状态,只取决于与该电梯直接相关的数据变量,具体而言包括该电梯外部的等待的乘客(小盘子)、电梯内部的乘客、电梯此时所在楼层以及此刻电梯的运行方向。具体的确定方向的逻辑是:如果电梯内有人,则保持之前的方向;如果电梯内无人,则检查电梯外的等待乘客,根据先来先服务的原则,以候客表中的第一位乘客的要求作为电梯的运行方向,即根据他的起始楼层和目的楼层确定电梯的方向;当然如果电梯内外均无人,则电梯方向置为0,即不动。

以上均实现在Elevator类中,根据指导书的意见,电梯的运行策略实际上可以抽象为从一组待选任务中选取一个来执行,所以可以创建策略类Strategy,并将其作为电梯的一个属性,通过更换策略类以便灵活切换运行策略。在本次作业中为了后续可能的工程迭代需求,以应用工厂模式组装不同的电梯,Strategy有其一定价值。但考虑到电梯本身的运行策略一般不轻易改变,且Strategy类与Elevator类存在众多变量交互,策略作为一个属性完全可以纳入电梯之中,所以在后续迭代中删除Strategy类,大大减小了类与类之间的耦合程度。

同步块与锁

各个进程间需要同步互斥的根本原因在于他们之间有共享对象,因此同步块的设置和锁的选择必须着眼于每个可能被共享的对象。

首先需要考虑的共享对象就是总的候客表(大盘子)和6个电梯各自的6个小盘子,而他们都是RequestQueue类,因此必须将候客表类构建为线程安全的类。这里想到操纵系统课中介绍的管程的概念,或许RequestQueue就像一个管程,所有与候客表有关的操作都应在管程中实现,而其他对象只是去调用它。

这次作业中选用每个对象内置的锁来实现同步互斥,对候客表类中涉及到的读写方法都使用synchronized加以保护,在其他类中调用此方法时不再考虑线程安全问题。

UML类图与协作

UML类图

UML协作图

HW6

迭代思考

这次作业相比于上次增加了两个需求:一是乘客不再指定乘坐的电梯,需要在Schedule类中实现调度策略;二是电梯可能会被reset。

调度策略

第一个需求由于上次已经存在了Schedule类,所以只需修改内部的分配逻辑,总体架构无需发生过大变化。为了尽量做到性能上的优化,分配时优先选择分配到距离该乘客最近且同向的电梯,如果没有电梯满足上述条件,其次再选择分配到内外人数之和最少的电梯。

实际上,电梯的调度问题是一个在变化环境下的动态调度、资源配置及随机最优控制的组合优化问题,其特征主要表现为不确定性、信息不完备性、扰动性、多目标性几个方面,其复杂程度是由系统所运行的环境与自身特性所决定的。甚至有人提出电梯的调度本身是NP难的问题,找到绝对的最优解在现实中是不可能的,也是没有必要的,在具体的工程实践中总是做到尽可能的优化。

重置操作

第二个需求是电梯可能会被重置(reset)。结合重置电梯的特点:将电梯内的所有人清空,取消电梯外所有的receive,可以发现重置后的电梯与一部“崭新”的电梯无异(除了不在1层)。所以对电梯的重置可以认为是将其从电梯的集合中删除,并在重置后启动一个新的电梯线程,并将其加入到电梯集合中。而这个电梯的集合应该由调度器(schedule)维护。

除此之外,电梯本身必须增加表示是否需要重置的属性,在每一次的状态更新中,都需要检查这一属性是否置位,若是则需要立即开始重置。这种做法可以保证电梯在接收到重置指令后可以在两次移动楼层操作内将所有乘客放出,并开始重置动作。

整体架构

相较于上次作业,本次迭代由于增加重置操作,导致电梯随时可能向外踢人,并取消接收到的receive,所以总的请求队列的生产者除了输入线程,还包括电梯线程。

删除Strategy类,在电梯类中直接实现运行方向判断与状态转移。增加ResetInfo类,将其作为电梯的一个属性,用于储存重置信息。增加了Myperson类,用于实现踢人时改变乘客的起始楼层。

同步块与锁

这次作业,共享对象类型增多。schedule调度器中存在两个ArrayList容器分别盛装可以被调度的电梯线程(即没有被重置)和这些电梯的小盘子。同时每个电梯线程都可能被重置,亦即都可能向schedule中添加新电梯,所以schedule作为共享对象也会成为电梯线程的一个属性;电梯随时可能向大盘子中踢人,所以总的等待队列(共享对象)也会成为每个电梯的属性。这样一来,各个类、各个线程之间的交互程度增大,对同步互斥的控制难度也会随之上升。需要把握一个原则,作为共享对象的类需要做到线程安全,以共享数据为中心确定同步控制范围,具体而言即是涉及到对共享对象的读写操作的均需进行同步控制。当共享对象自己管理好同步控制后,外部便无需额外保护。

本次作业中引入读写锁ReentrantReadWriteLock的使用。之前可以继续沿用synchronized的地方未加改动。两者都是Java语言中用于实现多线程同步的机制,但在实现方式和使用上存在区别。例如,ReentrantReadWriteLock比synchronized具有更高的灵活性,它允许更细粒度的控制。

UML类图与协作

UML类图

UML协作图

HW7

这次作业的整体架构、调度策略以及同步块和锁的设置与上次作业相比无宏观上的变化,故不再单独列出讨论,而只把关注的重心放在新增的需求上。

迭代思考

这次作业最大的难点就是增加双轿厢条件,需要实现如何让双轿厢在指定的楼层范围内移动、如何避免双轿厢相撞、如何实现双轿厢间的换乘。换乘操作与重置操纵有相似之处,都是达到一层后将电梯内的所有乘客放出,但不用取消接受到的receive。

至于如何让双轿厢不超过换乘层移动,需要从确定电梯运行方向的逻辑入手。如前文所述,一个电梯的运行方向首先是由其轿厢内的乘客,更确切地说是由乘客的起始层和目的层所决定的。具体实现是:第一,调度时保证只把起始层在电梯运行范围内的乘客请求加入到该电梯的候客表(小盘子)中,这样便可保证电梯不会超出范围去乘客;第二,电梯每到一层后,检查该层是否为换乘层,若是,便把目的层是换乘层和目的层不在电梯运行范围内的乘客全部赶出电梯,这样便可保证电梯不会超出范围去乘客。

避免相撞

那么在实现了双轿厢的换乘、固定范围内移动后,怎么避免同一井道内的两个轿厢就成为双轿厢问题的最后一个难点。这里参考了姜涵章同学的思路,考虑到两个轿厢只可能在换乘层相撞,便把换乘层抽象为两个轿厢共享的对象(实际上就是互斥信号量mutex)。同时需要实现,若电梯满足是双轿厢电梯,不需移动,位于换乘层三个条件,则需要向远离双轿厢的方向移动一层,以避免一个轿厢长时间占据换乘层不让出,导致另一个轿厢无法进入的情况。

UML类图与协作

相较于上次作业,增加了Occupied类避免双轿厢相撞。Occupied类与ResetInfo类的地位相似,都只与电梯线程交互,辅助电梯类实现相应功能。这次作业对应的UML类图和协作图如下所示。

BUG分析

高并发

在小于1s的时间内投入大量数据属于高并发情况。如果有这种情况,同时重置6部电梯,并且同时有大量乘客的请求到来,由于实现电梯重置是把电梯从schedule类中移除,这就会导致此刻schedule类中没有电梯,而无法分配乘客请求的情况,归结到程序中就会引发Wrong Answer的问题。解决方法是如果当前没有电梯可供分配,schedule调度器应该被阻塞而等待,直至被重置的电梯完成重置,有新的电梯可供调度时,再进行乘客请求的分配。

无法结束

最后时刻Reset所有电梯,便会导致reset后新加入的电梯线程无法结束的情况,在程序中也就是引发RTLE的报错。解决办法是让调度器延迟判断是否结束,等待足够长的时间后(即把所有重置后的新电梯加入schedule后),再将调度器线程结束,此时也会将调度器中所有的电梯线程结束。这样便避免了调度器线程结束过早,而无法结束电梯线程的情况。

DEBUG

由于多线程调度存在随机性,可能会出现bug无法复现的情况。且基于行断点机制的调试会对程序的运行行为产生较大影响,所以实际在debug时多线程比单线程程序困难得多。主要采用的方法是从一个小样例开始,观察实际输出与预期输出是否相符,逐渐扩充样例的难度,以期达到覆盖性分析,但事实上很难做到。通过在程序中增加输出状态语句,来跟踪进程的运行状态,但这种方式可能会导致在输入复杂时输出信息量过于庞大,对人工debug带来巨大挑战。

心得体会

层次化设计

面向对象程序中的层次化设计是从第一单元就引入的观点,并在表达式解析与化简的应用中得到充分体验,回到第二单元的多线程电梯系统,层次化设计对于保持架构的扩展能力有着重要意义,所以这是任何面向对象程序不变的追求。首先需要稳住大的结构,例如电梯线程、调度器线程和共享队列不能改变。其次,将易变的结构按照层次展开和逐步细化。例如充分考虑各种数据结构的特殊性,楼层是否停靠是一种特殊性,电梯的特殊性体现在普通电梯、双轿厢电梯、运动速度、载客量。同时调度也具有特殊性,体现在换乘、捎带、队列平衡、等待时间平衡等。

线程安全

直观来说,线程安全就是“线程**,你不用担心什么,只管做你的事情,保证不给你添乱...”。多线程程序怎么才会导致线程不安全呢?从写者-读者的角度来说,读写冲突和写写冲突才可能会造成线程不安全。设计出好的线程安全类也符合程序设计中的SRP单一职责原则。线程安全是在构思多线程并发程序时必须考虑的要点,而且是首要的任务,因为线程安全会直接影响到后续代码实现和检测,尤其是线程安全的debug工作非常困难。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值