BUAA-2024春-OO-第二单元总结

本文详细描述了一次编程作业中实现的多线程电梯调度系统,涉及UML类图、线程协作、共享对象同步、调度算法(评分制)以及处理doubleReset请求的复杂逻辑。作者强调了线程安全和代码层次化设计的重要性。
摘要由CSDN通过智能技术生成

构架分析

第一次作业

UML类图:
请添加图片描述

在第一次作业的实现过程中,我对多线程的实现方法依旧没有自己的完整认识,所以我的大致框架模仿了实验课的框架。首先在main函数中,启动三种共八个线程,分别是输入线程,调度器线程和六个电梯线程。其中输入线程负责将输入信息转化成一个个Request类对象,并保存在大等待队列中;调度器线程负责将大等待队列中的请求分配给每个电梯对应的小等待队列,这次作业明确指定了每个请求分配给的电梯,所以不需要设计调度算法;电梯线程负责处理其对应等待队列中的每一个请求,将所有人运输到目标楼层。

在这次作业中,共享对象主要有两类,一类是输入线程和调度器线程共享的大等待队列,另一类是调度器线程和电梯线程共享的小等待队列。在大等待队列的共享时,
请添加图片描述
请添加图片描述
负责向大等待队列增加请求和从大等待队列中取出请求的方法需要用synchronized上锁,防止边读边写的情况的发生。当输入线程还未接收完请求且暂时接受接受不到请求时,大等待队列可能会被调度器处理完,此时为了防止轮询,需要将调度器线程wait,放入大等待队列的等待列表中,直到输入线程向大等待队列中加入请求后或者真正输入结束setEnd后将调度器调度器唤醒,前者调度器就能接着处理新加入的请求,而后者执行后调度器就会在处理完大等待队列将所有小等待队列setEnd,告知所有电梯线程不会再有新的请求加入。
而在小等等待队列共享时,也同样需要满足不能边读取边写入,需要对所有访问小等待队列的方法上锁。

在电梯线程运行时,首先判断是否有人已经到达目标楼层需要在当前楼层出电梯,若有就将该请求从电梯的已载客列表中移除,代表该请求已经被执行完毕。接着判断电梯是否需要转向,若电梯已载客列表中依旧有请求或在当前楼层或当前方向剩余楼层中有请求需要上电梯,则不转向,否则转向。接着判断电梯是否会有请求在当前楼层加入到到电梯中。最后判断是否需要向移动方向移动一层,判断依据为载客列表中是否有人或者在当前方向剩余楼层是否会有请求加入载客列表。如果不需要移动楼层,若小等待队列已被调度器setEnd,则电梯线程结束,否则将其加入到小等待队列的等待列表中,直到调度器再次分配请求或setEnd后唤醒。

第二次作业

UML类图:
请添加图片描述
在第二次作业中,增加了reset请求,在reset时需要将所有电梯已经receive的请求释放回大列表,而已经被电梯载客的请求若依旧为到达目标楼层则需要改变fromFloor重置成一个新的请求加入到大队列,让大队列重新分配这些新的请求。其中需要注意的是电梯在reset时是不能接受请求的,所以在reset的过程中电梯线程需要拿到对应请求列表的锁,防止调度器线程在reset过程中执行该请求列表的addRequest方法。
在这里插入图片描述
另一个需要注意的点是第一次作业只需要大队列setEnd后就可以将小队列也setEnd并在处理完剩余请求的分配后结束调度器线程,这是因为大队列中请求的来源只有输入线程,只要输入线程结束,大队列就不会新加入请求,调度器在分配完剩余请求后就不会有新的请求,小队列也不会接受到新请求,所以就可以结束调度器线程,但是第二次作业中,大队列中请求的来源除了输入线程外,还有电梯reset时释放的需要重新分配的请求。所以在判断是否要将调度器线程结束并将小队列setEnd时,还要加上判断所有reset请求是否执行完成,只有在reset执行完后,才能保证大队列不会接受除输入线程以外的线程分配的请求。同时在防止调度器轮询方面,也有新的要求,第一次作业中,只有在大队列未被setEnd并且大队列为空时需要wait,现在在大队列被setEnd且为空后还需要在reset请求未执行完时也sleep,直到一个reset执行时将请求分配给大队列时唤醒。

第二次作业的另一个要求是没有指定每个personRequest应分配给哪个电梯,所以需要我们为调度器设计一个调度算法去进行personRequest的分配。在这次作业中,我选择的一种评分制的调度算法,用一些指标去计算电梯的分数,最后将personRequest分配给分数最高的电梯。我将电梯的所有属性存到对应的请求列表中,并在电梯属性改变时马上改变对应请求列表的属性(这个过程需要对请求列表上锁,防止在电梯属性改变后,调度器刚好执行到访问请求列表中对应的属性的指令,此时电梯线程还未更新请求列表的属性,导致调度器访问到的属性与电梯实际属性不同)。

以下是我选择的一些评分指标(若指标越大,得分应越低,则标为“-”,反之标为“+”):
第一个指标是电梯receive的请求数目(-),我们可以设想一个情况:在某一时刻突然接受到大量请求,此时某电梯在接受到第一个请求后,由于两个请求分配的间隔时间极短,所以电梯可能还完全没有开始执行动作,此时所有电梯除了receive到的请求,其他属性完全相同。如果receive到的请求数目不是评价指标,那么所以请求列表分数线相同,那么会将请求固定分配给某一电梯,这显然是不对的。
第二个指标是电梯还可载客量(+),即满载人数-已载客列表的数量,这遵循了平均分配的思想,既不会让某个电梯人数相比其他电梯过少,也不会相比其他电梯过多。
第三个指标是是否在reset(-),显然在reset的电梯,还需要一段时间才能执行完reset请求,这段时间由于电梯拿到了请求列表的锁导致内请求列表无法即时接受到该请求,所以电梯也无法即时为该请求做出行动,导致时间较长。
第四、五个指标就是电梯在不停的情况下,运行到该请求的fromFloor所需的时间(-)和层数(-)。显然时间越长,层数越大,电梯性能必然越差。
在得到所有指标后,为每个指标分配一个合适的系数,就可得到最终得分。

第三次作业

UML类图:
第三次作业与第二次作业完全相同

要求:第三次作业新增了doubleReset请求,会在执行后将一个电梯变成两个轿厢,分别只能在指定楼层及以下楼层后指定楼层及以上楼层运行,且不能同时到达指定楼层,否则两轿厢会发生相撞。

我选择了在doubleReset执行时,删除所有小请求列表的容器中原电梯对应的请求列表,并加入两个新的空请求列表,接受两个轿厢receive到的请求,同时开始两个新的电梯线程,其他执行过程与第二次作业的reset基本相同。在doubleReset执行完后,将原电梯线程结束。
在实现上述目的时,我碰到了很多问题:
1.如何保证在doubleReset时原电梯请求列表不会接收到请求?
这里我选择在doubleReset刚开始时,就对该电梯请求列表上锁,然后将该电梯请求列表中的isDoubleReseting属性置为True,直到在结束doubleReset时,才将其置为False。在调度时如果某请求列表的isDoubleReseting属性为True,直接跳过该请求列表,即personRequest必定不会分配给这个正在doubleReset的电梯。可是这样也会产生问题,前面说过调度时得到的电梯信息能保证和电梯的属性同步,但是我不能保证调度时得到的信息在调度结束将请求分配给小队列时的电梯信息是同步的。这个问题在第二次作业就有了,但是第二次作业中信息的不同步只是会稍微影响评分,并不会产生错误;但是第三次作业中由于isDoubleReseting属性直接决定的是否能够分配,所以如果它没有绝对的同步,就会产生错误。举个例子:在调度器查找请求列表的isDoubleReseting属性时,电梯还未开始doubleReset,所以得到的isDoubleResting属性为False,在计算评分后又恰好分配给该电梯。但是就是在查询isDoubleReseting属性到将请求分配给该请求列表之间的时间,该电梯开始doubleReset,这时电梯会将对应请求列表上锁,所以之后调度器分配请求时会等doubleReset结束释放锁,此时再讲请求分配给该电梯请求列表,导致明明电梯doubleReset结束了还能receive请求的错误。
那么怎么去解决这个问题呢?我最先思考的就是既然是因为调度和分配之间不能做到同步,那直接将这两句放在请求列表的锁中,在执行这两个过程时,电梯就无法开始doubleReset,那么自然不会产生错误。但是很快这个想法就被我舍弃了,因为只有在调度完成后才能知道到底应该分配给哪个请求列表,所以在调度前就将该请求列表上锁是不可能的。那么将所有请求列表上锁呢?这虽然可以实现,但是却对性能是重大打击,因为将所有请求列表上锁,那么所有电梯都会等待调度完成后才能开始执行reset,更严重的是只要有一个电梯在reset,那么所有大队列中的请求都会等待reset执行完才能开始分配,万一一直在reset一部电梯,这会导致其他电梯也无法接收到请求并开始运行,大大打击了性能。
所以只能在调度完成后,再将该请求列表上锁了。为什么要上锁呢?在面对我之前提出的那个bug例子时,如果此时拿到了锁,那么意味着doubleReset已经执行结束,那么将请求再次调度分配给别的电梯即可,但是如果还是使用原来的调度算法,那么依旧会碰到相同的问题,所以我选择直接将其分配给该电梯doubleReset完后产生的那个能接到该请求的那个轿厢,这样就能彻底解决问题。如果未拿到锁,这意味着doubleReset为开始,那么此时将请求分配给该请求队列,即使马上开始doubleReset,电梯也能将该请求重新放回大队列中并接受下一次分配。

2.如何保证新产生的轿厢不会在doubleReset时receive请求
这有一种最简单的实现方式,即在输入reset-end之后再将两个轿厢的请求列表加入到容器中。即时所有电梯都在reset,那么也只是调度器先将请求分配给电梯,在结束doubleReset后再分配给电梯的某个轿厢

3.如何保证两个轿厢不会在共享楼层相撞
既然该楼层是共享的,不能同时到达完全可能看作是不能同时拥有一个共享对象,即共享楼层。所以两个轿厢可以拥有一个共享对象,在轿厢移动前,判断轿厢移动的目的地是否是共享楼层,如果是,就尝试获取那个共享对象的锁,获取到代表另一个楼层不在共享楼层,那么直接移动即可;获取不到就代表另一个轿厢在该共享楼层中,需在另一个对象移走并释放锁后才能得到锁,并移动到共享楼层。

debug方法

在多线程作业中最容易出现的bug就是线程无法结束问题了。这种bug大多是由于在某个对象的等待队列中无法被唤醒导致的,所以我在所有wait语句前输出id+“ wait”,在wait语句后输出id+ “ wake”。看看到底是哪个线程在sleep后无法被唤醒,然后根据输入去找为什么没有对应的唤醒语句的问题。还有可能就是结束的条件始终无法达到,导致线程一直在循环判断结束条件,面对这种问题我会现在所有线程的退出处输出线程名+“ end”,定位到无法结束的线程,再将该线程的结束条件的取值输出,看看哪个条件没有满足,接着去找该条件无法满足的原因即可。

心得体会

线程安全:锁这个东西用好了对多线程程序有很大的帮助,但是用不好会产生很多莫名其妙的问题。所以在编写程序之前我们就应该思考好到底哪些共享对象的哪些方法需要加锁,在什么时候wait,有在什么条件下才能notify,方能让死锁问题直接胎死腹中。
层次化设计:这次作业中对代码的逻辑要求极高,即使两条互相砍死不相关的语句之间稍微换一下顺序,就会产生难以预料的错误,所以对代码的层次化设计要求极高,将代码严格按照每一段各自的功能进行封装就显得极为重要。

  • 18
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值