面向对象第二单元总结:多线程

面向对象第二单元总结:多线程

第二单元痛苦的三次迭代完成,经过三周时间对多线程的编写、调试、优化,我对线程之间的互斥共享原则,以及实现并行的程序设计架构,都有了更深的了解,

本单元作业中具体如下:

目标简介

作业一:完成乘客请求,即对于每个乘客请求(起点层,终点层),需要调度电梯将其完成,并将必要的运行信息通过输出接口进行输出,此次作业指定了每个乘客乘坐的电梯

作业二:同样需要完成乘客请求,与第一次不同的是不再指定电梯,需要自行调度分配。除此之外还多加了reset功能,可重置承载人数和运行时间等,即输入重置相关信息后,电梯需尽快进行1.2秒的重置时间,期间处于静默状态,不能有人且必须关门。难点之一在于电梯维护后对乘客请求的重分配

作业三:增加一个新的重置请求,可以将电梯变为双轿厢电梯,两者有一个换乘楼层且不能同时处于换乘楼层,可通过协作完成任意请求

第一次作业

主要思路
输入类

第一作业框架的构建依然十分困难,在对多线基本没有了解的情况下更是难以开始

好在有实验给出了基本框架的指引,个人认为输入类也是本次作业最能体现多线程的地方。

生产者-消费者模型。调度器是生产者,各个电梯是消费者。每个电梯都有一个“盘子”,用来和调度器交换乘客请求,而由于在第一次作业中输入较为简单,所以本人直接将大盘子即输入类略去,由调度器直接输入而分配到各个小盘子。

调度器示意图

(借自往年大佬示意图orz)

调度策略

因为本次作业指定了用哪个电梯,所以无需考虑电梯之间的调度。

电梯内乘客

经过上网搜索及考量,采用的是往年常用的look策略,这是一种较为均衡的策略,而本人在最后实现中又略进行了变形和优化,总的来说可以分为以下步骤:

  • 首先进行主请求的初始化,寻找请求中与电梯所在楼层最远的出发楼层为targetFloor

  • 当电梯每到达一个新的楼层时,尝试根据“盘子”里的内容(即乘客请求队列)收入乘客,如果此时满载则忽略,如果不是则捎带同方向出发地距离倒序乘客,直到满载或本楼层请求全部处理完。

  • 到达目标楼层后,判断本楼层是否有请求,有则依然优先将targetFloor置为距离最远的到达楼层,没有则置为-1,进入下一次循环(即重新初始化主请求)

运行方式

经过调度以后运行起来就比较简单了,循环体如下:

  • 判断是否退出
  • 正常退出乘客
  • 调度
  • 进入乘客
  • 输出信息
  • 根据targetFloor判断上行或下行
输出类

为了简洁,处理进入与退出乘客时每次更新维护offPassenger和onPassenger的hashMap,输出类只需根据两者信息和电梯相关信息就可输出所需要的信息。

细节问题
互斥的实现

这次作业是第一次,细节问题主要都体现在输入和电梯“盘子”之间多线程运行的处理上,因为关系到线程之间的并行,对原子操作的使用不当很可能出现死锁等意外现象,甚至很多时候这种异常都是隐蔽的,难以复现出来,下面说一下困扰我很久的终止问题。

线程终止时应当满足:

  • 电梯内没有乘客且请求表里没有乘客
  • 请求表退出

而刚开始处理时,我将synchronized框为整个循环体,这样处理的结果是每次循环时大量占用资源造成不必要的浪费。在查阅资料之后,本人采用了较为稳妥的方式进行退出,即正常等待调度器通知结束而最后空转一次(仅一次,如果长时间空转会出现CPU超时的错误,这在第三次作业会具体说到),再进入循环时可以正常退出。

此终止问题也应当是第一次作业中

bug分析

第一次作业遇到的bug不多,且多为疏忽笔误导致,最后强测没有出问题。

印象最深的错误是在上下行时未判断targetFloor是否不为-1,最后掉到零楼去了(乐)

性能方面

这一次作业指定了需要乘坐的电梯,因此一般来说只有电梯内部的调度,能提升的部分不多。

但是依然有同学实现了更高端的优化——“量子电梯"的概念,即每次接完人以后等待若干时间,在特定情况可以极大节省时间,个人因为实力不够(主要原因)且此策略似乎有点脱离实际设计本身而没有下手修改。

hack方面

本次作业hack并未成功,在尝试hack中会尽量考虑边界值的特殊情况,例如全在同一时刻(可以全在五十秒)出现指定同一电梯的多个请求,且这些请求的楼层分别为 10-11,9-11,8-11,……1-11,如果没有采用很好的策略会导致超时。

类图

由如上说明构成类图

电梯类:

image-20240415151040621

输入和输出类:

image-20240415151122608

屏幕截图 2024-04-15 151112

调度器(策略)类:

image-20240415151211446

协作图

第一次作业时序比较简单,主线程启动调度器线程,调度器再启动电梯,等输入结束后通知电梯结束,同时结束调度器,最后再结束主线程,因此从下一次开始绘制协作图。

第二次作业

主要思路
输入类

因为需要重置,而重置吐出的乘客需要重新进行分配,所以新增RequestTable即请求的大池子,读入时先将Request放入大池中,

电梯类
  • 因为需要重置,新增重置行为,负责吐出电梯内的乘客到总表
  • 在自己电梯waitLine中增加剩余容量,所在的楼层,目标楼层等信息,这就是本次作业中调度器与电梯交互的方式,每次需要相互只需读取waitLine即可(丑陋无比),但是不用再多加一个新的类用于交互,实际上还是加新类(也就是大多数同学所用的策略类)比较清晰。我的交互具体内容如下:
fromFloortoFlooridinformation
floorNum00targetFloor
floorNum01floor
floorNum02restCapacity
floorNumfloorNum-重置相关信息,没有则清空
调度策略

电梯内部调度策略保持不变,增加调度器分发时的电梯选择

电梯间调度策略
  1. 首先判断是否有空闲的电梯,寻找离请求出发地最近的剩余容量不为0的电梯,没有则进行下一步。
  2. 判断是否有同向电梯,寻找离请求出发地最近的同向且容量不为0的电梯,没有则进行下一步。
  3. 寻找离出发地最近的同向且容量不为0的电梯,没有则下一步。
  4. 此时全部电梯都满,轮转分配,即取下一个电梯
细节问题
互斥的实现

此次作业锁的实现数量有所提升,重置命令的出现使进程之间的交互更为密切,因为大多数情况下都需要读写同时进行,因而一概采用synchronized进行包围

  • RequestTable的互斥,大池有三个类需要共同使用,分别是InputHandle输入时,Elevator重置吐出乘客时,Allocator读取请求分配时,在三者使用到时都需实现互斥
  • WaitLine的互斥,因为个人将其设置为进程通信的唯一渠道,所以同步次数非常多,具体体现在调度器通知电梯重置,分配请求;电梯读入重置信息,读入请求的时候需要实现互斥

同时终止条件也发生变化,因为当输入终止时可能依然有电梯在进行重置,因此在RequestTable中加入ResetNum属性,判断目前剩余的重置命令数,使用原子化修改和读取方法,读入重置或完成重置时对其修改,最后判断是否为0,即调度器结束条件为:

  • RequestTable读到结尾
  • RequestTable统计的剩余重置数为0

image-20240415205906620

再将WaitLine设置为end,终止各个电梯进程。

bug分析

此次强测和互测共有两个问题

1.第一个在于每次重置时电梯到达超过两个楼层,而在本地运行时相同问题却没有问题,于是就开始了痛苦的多线程debug环节()

由于提示是移动过多,而考虑到直接用强测数据复现的难度,因此只能自己捏数据。

什么情况下最容易发生移动过多的情况?考虑以下情况

  • 两个电梯同时被重置
  • 其中后重置的电梯的运行时间为0.2秒(最短)

经过多次的尝试,最后选择了同时重置五个电梯设置运动时间为0.2秒,且再次同时重置两个电梯的情况,成功复现出bug

原因是刚开始设计时选择接收到重置请求后选择等待电梯做出响应,再单独处理吐出的乘客。这时候如果同时又有一个电梯请求重置且其运行时间为0.2秒,而正在等待响应的电梯从等待到响应往往超过0.4秒,因此这时候另外一个电梯可能到达超过两个楼层。

2.还有一个bug在于分配策略问题,这是互测的热门数据,在五十秒时重置五个电梯并且塞入60个请求,会造成超时的情况,原因在于分配策略不当。这个就比较好debug了,在运行时发现后面的请求全塞入一个电梯里,不符合原本期望。因此在每个分配步骤判断设置断点,最后发现是寻找最近的电梯并未判断剩余容量不为0,最后导致全部塞入同一个电梯内。

hack分析

用了上述热门的数据成功hack了三个人,双刃剑了属于()

类图

由如上说明生成类图

电梯类:

image-20240416171225276

输入和输出类:

屏幕截图 2024-04-16 171319

image-20240416171327309

调度器(策略)类:

image-20240416172056862

调度器是连接输入电梯和部分输出的桥梁。

时序图

UML时序图

第三次作业

主要思路

这次作业要添加的不算多也不算少,因为双轿厢中每个轿厢与原来电梯的逻辑基本类似,所以只需实现一个双轿厢管理类和基本类似的双轿厢电梯类。

双轿厢电梯类

大体和电梯相同,在原有电梯基础上加了两点判断:

  • 即将到达换乘楼层时,如果由管理类传递的换乘楼层占用的标记信号为真,则等待直到信号被释放,到达后再将其设为真,防止另外一个电梯到达。
  • 到达换乘楼层且电梯内乘客不为空时,释放所有乘客归于大池子(类似电梯的重置行为),由调度器再继续分配。
管理类

因为双轿厢为两个电梯共用一个waitLine,因此再用原来往WaitLine中添加信息的方式就实在太奇怪了。设置管理类可以实时保存两个电梯目标楼层,楼层和剩余容量等信息,每次电梯内数据变化时可以实时反映到管理类的属性上,而外部调度给双轿厢电梯时只需拿到管理类就可以获取信息,这就是双轿厢在这次作业中与外界交互的方式

调度策略
电梯内调度

主要为双轿厢内的调度,因为可能存在暂时无法到达换乘楼层的情况,因此在初始化主请求楼层时,如果换乘楼层被占用则最后选择其中的请求,优先处理其他的请求。

电梯间调度

大体还是和第二次作业相同,而双轿厢电梯省电的特性使得不同点在于每个步骤优先选择双轿厢电梯,即最终为以下步骤

image-20240416200310981

细节问题
互斥的实现

不同于前两次作业,在双轿厢相关的互斥上根据上网资料引入了Atomic类型数据,虽然不多,这里主要用到AtomicBoolean,AtomicInteger

  • 管理类中用于与外界交互的信息,例如targetFloor等,都用AtomicInteger来保存
  • 管理类中用于两电梯供用的换乘楼层占用信号,用AtomicBoolean来保存,实现线程安全
bug分析

这次的bug主要集中在强测前,出现了一堆CPU超时。

根据助教给出的建议,CPU超时多半为线程处于轮询状态而不断空转导致,而本次作业只多加了双轿厢电梯线程,因此考虑是多轿厢产生空转。

在run方法第一行加入:

TimableOut.println("****" + type);

发现出现几十万行的****输出

因而进一步定位发现每次启动两个电梯后都会立即开始运行,原因是WaitLine判断是否为空时每个电梯需单独判断,而不是按照原本普通电梯的判断方式,debug结束。

还有一个莫名被hack的点,很喜欢bug人的一句话:啊?

image-20240416164214372

再也不复制粘贴了()

hack方面

hack不到一点,尝试依旧在五十秒的一堆请求并未成功,感觉大家都在性能策略方面有所注意了

类图

其余都相同,只放发生变化的

双轿厢电梯类:

image-20240416202618833

由管理类统一进行管理

管理类:

image-20240416202719976

通过Allocator中的DoubleManagers与其相关联

时序图

UML时序图2

心得体会

几次作业稳定和易变的点

电梯内部调度算法一旦建立就基本不会发生改变,而类似双轿厢电梯的其他因素加入使调度器的策略屡屡发生改变以适应需求。

感悟

这个单元的三次作业做得很艰难,遇到的bug也很多,个人认为自己这一单元层次化结构和面向对象的思维体现是不够的,前两次都为了避免类数量过多而反复使用WaitLine变量等,导致调度器类和电梯类等都及其臃肿,直到第三次管理类的出现才略有改观,希望之后的作业在第一次作业就能有好的思路。

不管怎么说,这个单元让我对线程有了初步了解,对线程的初步探索让我从实践意义上接触了线程之间的并发、同步、安全、协同等概念和机制,掌握了原子化模块等锁的机制实现线程安全,让程序在并发与互斥的情况下顺利进行,也学会了一些在难以复现状况下的debug方法(虽然说还是很折磨),无疑对我整体编写代码能力提高是很有帮助的。

最后提个建议,希望之后中测强测互测等测试能多跑几遍,中测每次多跑几遍能让部分没有意识到bug的及时发现问题(比如我),强测互测多跑几遍有利于实现最后得分的公平性。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值