BUAA-OO-Unit2-总结

BUAA-OO-Unit2-总结

1. 作业架构

1.1 hw5

第五次作业要求我们实现一个具有六部电梯的运输系统,使得其能够正确完成输入的请求。
UML图
在这里插入图片描述

同步块的设置和锁的选择

本次作业我为六个电梯均设置了一个缓存队列WaitingList,当请求输入时,将其加入相应电梯的缓存队列中,电梯线程通过Strategy提供的choice来做出下一步动作,处理缓存队列中的请求。由于WaitingList是由输入线程和电梯线程共享的对象,所以将其中的所有方法加锁,这样就可以保证每次只有一个线程来访问缓存队列。

调度器设计

由于本次作业电梯请求会直接输入给相应电梯的缓存队列,所以只需要在电梯一级进行调度。故我设置了一个Strategy类来调度电梯的运行。Strategy类根据电梯现在的状态和缓存队列的状态,通过Look策略给出choice,电梯借助choice来决定自己的下一步动作。
LOOK策略

  • 电梯初始设定方向后,按此方向移动。
  • 到达楼层时,检查是否需要开门:若内部有人需出梯,则开门;若该楼层有人需上梯且方向与电梯相同,则开门让其进入。
  • 之后,若电梯内仍有人,则继续向当前方向移动至下一层。
  • 若无人且请求队列非空,且请求来自电梯前方楼层,则继续原方向运动;若请求均来自后方楼层或该楼层请求目的地在后方,电梯掉头并重新判断开门需求。
  • 若请求队列为空且输入未结束,电梯停在该楼层等待请求。电梯等待时方向不变,新请求唤醒电梯时仍按原方向运动。
  • 若请求队列为空且输入结束,电梯线程终止。
1.2 hw6

hw6新增RESET请求和RECEIVE要求,这意味着我们无法使用往年简单且行之有效的自由竞争策略,必须使用二级调度的调度策略,来完成电梯重置及其调度。
UML图
在这里插入图片描述

同步块的设置和锁的选择

为了完成RESET请求,本次作业相较于上次新增了TotalQueue共享队列和Dispatcher线程进行乘客的一级调度。由于TotalQueue是输入线程、一级调度线程和电梯线程的共享对象,所以对其中所有方法加锁,这样就可以保证每次只有一个线程来访问共享队列。与上次一样,CacheQueue是一级调度线程和电梯线程的共享对象,所以对其中所有方法加锁以满足每次只有一个线程来访问缓存队列。

调度器设计

本次迭代使用二级调度策略,增加Dispatcher线程以实现一级调度,将TotalQueue中的乘客分配到相应电梯的缓存队列中。在电梯一级使用二级调度器Strategy将缓存队列中的乘客加入电梯中,实现电梯的运行。
一级调度策略
获取六部电梯的缓存队列,缓存队列中就是电梯仍未完成的请求,根据电梯未完成请求大小,每次都将TotalQueue中的请求分配给未完成请求数最小的电梯,以实现均匀调度。
二级调度策略
沿用第五次作业的LOOK策略,实现电梯一级的调度。

1.3 hw7

第七次作业新增请求DCERESET,需要我们能在hw6的基础上让电梯可以重置为双轿厢电梯
UML图
在这里插入图片描述

同步块的设置和锁的选择

由于本次作业新增请求只是对RESET指令进行了扩展,因此可以沿用上次作业的架构,只需要在其基础之上增加两个电梯线程。所以本次作业的同步块的设置和锁的选择与上次作业相同,TotalQueue是输入线程、一级调度线程和电梯线程的共享对象,所以对其中所有方法加锁,这样就可以保证每次只有一个线程来访问共享队列。CacheQueue是一级调度线程和电梯线程的共享对象,所以对其中所有方法加锁以满足每次只有一个线程来访问缓存队列。

调度器设计

本次迭代延用了上次作业的二级调度策略,使用Dispatcher线程以实现一级调度,将TotalQueue中的乘客分配到相应电梯的缓存队列中。在电梯一级使用二级调度器Strategy将缓存队列中的乘客加入电梯中,实现电梯的运行。

如何实现双轿厢的两个轿厢不碰撞?

新增TransFloor类即可解决这一问题。将TransFloor设置为电梯A、B的共享对象,当A、B电梯中的一个到达TransFloor时,这个电梯就拥有了这个TransFloor的锁,另一电梯就不能进入这一层。当这个电梯离开时就会释放TransFloor的锁,此时另一电梯才能进入TransFloor。

2. Bug分析

2.1 围师必阙

将五个电梯一起RESET的同时输入大量请求,此时如果在调度机制处理不当的话,就会导致所有请求都被唯一的一个没有在RESET状态的电梯全部接收,导致完成所有请求的时间超过课程组设定的220s的上限,出现TLE的Bug。
解法
在缓存队列中设置buffer队列,不论电梯是否处在RESET状态,请求都可以分配到相应的buffer队列中。当电梯完成RESET之后,再将buffer中的请求交由电梯来完成。

2.2 轮询

线程在运行过程中由于没有及时休眠,导致其一直处在空转的状态,这种情况下可能输出结果是正确的,但是会出现CTLE的问题。
如何发现轮询
使用print大法,在线程中加入print语句,观察是否会同时大量出现输出相同语句的情况,若出现则发生轮询。
如何解决
检查线程的执行逻辑,该休眠时及时休眠,避免CPU空转。

2.3 死锁

线程不安全会导致死锁的出现,此时输出会出现卡住的情况。这种问题多是由于争夺同一共享对象的两个线程,一个争夺到锁之后,后续处理过程中就没有再唤醒的操作,另一个线程因此就处在永久休眠的状态中。
解决方法
在线程争夺到锁之后,要退出时一定要增加唤醒其他线程的操作,避免死锁。

3. 心得体会

3.1 线程安全

由于多线程的随机性,在完成Unit2时时常会出现de不出的bug,本地测了很多次都无法复现bug,因此在面对多线程问题时一定要格外谨慎。我们首先要确定共享对象是谁,有哪些线程会对共享对象进行读写操作;之后再确定加锁的模块,保证线程之间的同步与互斥。

3.2 层次化设计

本单元我使用的是生产者-消费者的设计架构,生产者为输入线程,消费者为电梯线程,从生产者到消费者的流程由调度线程来控制。这个设计架构的好处是结构简单清晰,尤其是对于多线程问题,使用生产者-消费者架构后就可以省去大量的时间去思考如何确保线程安全,只需要在共享对象上加锁就可以了。同时,我也使用了二级调度的方法来完成从生产者到消费者的调度过程,二级调度的好处是可以让调度更高效,代码更简洁,各级调度器只需要完成各自的任务就可以了,省去了思考下一步怎么办的时间,下一步交由下一级调度器去考虑就可以了,符合高内聚、低耦合的设计原则。

3.3 总结

多线程问题是一个在混沌状态中的问题,程序可能出现很多种意想不到的情况,一定要进行充分测试

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值