OO第二单元作业总结

陈一飞-22373462 学生 2024-04-19 17:16:59

OO第二单元总结

前言

本单元作业主题是多线程,主要任务是实现电梯调度。对鼠鼠来说多线程是一个巨大的挑战,尤其是线程安全的处理bug频出,让鼠鼠苦不堪言。每一行都是鼠鼠充满血泪的奋斗史。

代码架构

UML类图

img

时序图

img

第五次作业

题目要求

本次作业的目标是模拟多线程实时电梯系统。楼座内有多部电梯,电梯可以在楼座内1-11层之间运行。系统从标准输入中读入乘客请求信息,然后被分配请求的电梯会经过上下行,开关门,乘客进入/离开电梯等动作将乘客从起点层运送到终点层。

复杂度

img


复杂度主要都在电梯线程当中,电梯的run方法和setDirection方法过于冗杂。

设计思路&&线程交互

本次作业乘客乘坐哪部电梯已经分配好了,所以这一次作业我们不用设计电梯调度策略,只需要把乘客分配到对应的电梯乘客列表里就行了。
本次作业我采用生产者-消费者模式,设计了三个线程类,分别是输入线程、调度器线程和电梯线程。线程之间交互依赖于请求队列(输入线程和调度器线程共享)和侯乘队列(调度器线程和电梯线程共享)。

输入线程

输入线程与调度器线程共享一个请求队列。输入线程不断读入输入并添加到请求队列里面;如果没有结束也没有输入就等待。读入完成就设置结束退出线程。

调度器线程

输入线程与调度器线程共享一个请求队列,电梯线程与调度器线程共享侯乘队列。调度器线程从请求队列里面不断读取乘客,并分配到对应的电梯侯乘队列当中。当请求队列结束时就把侯乘队列也设置成结束。

电梯线程

与调度器线程共享侯乘队列。电梯线程运行策略如下:

  • 首先判断乘客列表和侯乘队列是否为空
    • 是 :再判断侯乘队列是否结束
      • 是 :电梯线程结束
      • 否 :等待
    • 否 :设置电梯运行方向
      在设置运行方向这一块我采用了look策略,分为以下三步:
      1.若电梯中有乘客要到达的楼层在当前的方向上的前面,则保持当前方向不变。
      2.若1不成立,则若候乘表中有乘客出发的楼层在当前的方向上的前面,则保持当前方向不变。
      3.若1,2都不成立,则电梯原地等待。
  • 判断是否有乘客要出去
  • 判断是否有乘客要进来
  • 运行一层

需要注意的是,需要先判断出人再判断进人,防止可能出现的有人进不去情况。

线程安全的处理

因为不同线程之间有共享对象,为了保证线程安全,我们需要设置同步块和锁来保障线程安全。我对RequestQueue类里面的所有方法都上了锁,保证同一时间只能有一个线程进行读写。addRequest、getOneAndRemove、isEmpty、isEnd、和setEnd方法都有notifyAll语句,用来唤醒等待中的电梯或者输入线程。notifyAll语句不能用太多,不然会导致CPU时间爆掉。
电梯线程在运行的时候要锁住侯乘队列,否则就会出现问题。在互测出现了一个bug是电梯线程在运行的时候没有锁侯乘队列,导致了调度器线程往里塞人的同时电梯线程还在出人,可能会导致乘客被提前删去没有到达目的地的情况。

第六次作业

本次作业大寄特寄呜呜,出现了好多bug,到最后也没有完全修改。Anyway还是非常感谢wjj和gpf佬们的帮助,帮我解决了一部分问题。orz

题目要求

接收到重置指令的电梯需要尽快停靠后完成重置动作,再投入电梯系统运行。为安全起见,电梯重置时内部不可以有乘客,且重置动作需要时间T = 1.2s。
本次作业需要为乘客分配电梯,并且新增了reset和receive要求。

复杂度

img


相比上次,也就多了reset功能,代码量总体上并没有增加太多。

作业架构和线程交互

还是沿用第五次的作业架构,只需要实现新增功能的部分即可。
本次作业还是生产者消费者模式,只不过生产者新增了电梯,因为电梯在reset的时候也可以把人踢出去。
因为有了receive的约束,相当于把自由竞争给ban了。一个局部最优的解决办法是实现量子电梯,把电梯深克隆一遍去跑选择一个用时最短的电梯。

输入线程

当输入的请求是乘客请求时,和第五次作业处理一样。
当输入的请求是reset请求时,我调用了调度器线程的setReset方法去实现这一功能。

调度器线程

  • 调度策略
    因为需要分配电梯,所以在调度器线程中要实现这一功能。我的做法是平均分配,因为量子电梯的实现实在是写不出来呜呜。当分配的电梯正在被重置的时候,就去分配下一个电梯。

  • reset功能的实现
    新增setReset方法,用来实现对应电梯reset。

    public void resetElevator(int id,int max,int moveTime) {
          synchronized (processQueues.get(id + 1)) {
              elevators.get(id).setMax(max);
              elevators.get(id).setMoveTime(moveTime);
              elevators.get(id).setReset();
              processQueues.get(id + 1).notifyAll();
          }
      }
    

    注意:这里得有一个notifyAll,唤醒等待中的电梯。

  • 结束条件的改变
    因为新增了reset,不能沿用第五次作业的结束条件即请求队列为空且结束,因为这样可能会导致电梯把人踢出去后没有重新分配,乘客没有完全送走。
    我的做法是在第五次作业的基础上增加了判断当前reset请求数目是否为0,是的话请求队列就可以结束,否的话请求队列就等待。

    电梯线程

  • 新增resetFlag标志电梯是否正在被重置。

  • 增加了设置电梯参数的方法。

  • 电梯行为变化
    把reset看作电梯的一个行为,如果resetFlag为true,就执行reset操作,否则就按照第五次作业的逻辑执行。reset是需要把人全部赶走,同时电梯的侯乘队列也要清空重新分配电梯重新receive。
    此处需要注意的是,可能读取了reset请求之后电梯参数已经改变,但电梯还在运行中,导致reset还没有begin但是已经用了reset之后的参数。我的做法是做一个特殊判断,当resetFlag为真的时候使用原来参数,为假的时候使用现在的参数。

    线程安全的处理

    电梯在reset的过程中,要锁住请求队列和侯乘队列,防止出现边踢人边进人的情况。

    第七次作业

    题目要求

    本次作业新增了重置为双轿厢电梯的新功能。第二类重置请求将电梯修改为双轿厢电梯,重置参数包含需要重置的电梯ID,换乘楼层,两个轿厢的相关参数(移动一层的时间和满载人数)相同。当重置完成后,轿厢 A 默认初始在换乘楼层的下面一层,轿厢B默认初始换乘楼层的上面一层。看起来实现似乎也不是很难,但是分数却是惨不忍睹。

    复杂程度

    img


    电梯线程的go方法复杂度提高,因为涉及到双轿厢相撞问题。同时出人的条件也更加复杂

    作业架构和线程交互

    如何实现双轿厢电梯?当出现DCreset请求的时候,需要新增一个电梯线程。为了减少这部分的麻烦,我选择的是在一开始就新建12个电梯线程,初始时前六个是正常电梯,后六个暂时无法使用。在出现DCreset请求的时候,就把前六个当中的某一个设置为A轿厢,后六个中与之对应的电梯设置为B轿厢,这时候就可以实现双轿厢电梯的功能了。

    输入线程

    一共包含三类输入,分别是乘客请求,正常reset请求和DCreset请求。关于新增的这类线程处理,我还是采用了调用schedule中的方法去进行处理。

    调度器线程

  • 增加了处理DCreset请求的方法,对电梯的各个参数进行改变。

  • 在电梯分配策略时,我还是沿用了平均分配的方法,只不过判断条件更加复杂(比如如果分配的时双轿厢电梯,需要判断双轿厢电梯是否能到达乘客现在所在楼层),while循环内容太大,可能循环很多次也不会找到一个合适的电梯去分配,导致出现了轮询。
    改进方法:多进行一步判断,在即将分配电梯的前check一遍,防止在分配过程中某部电梯被reset送不走人。

  • 调度器线程结束的条件判断我改成了请求数是否为0。当输入线程接收到一个乘客请求时就请求数就加一,成功送走一个乘客就减一。其他条件满足,但是请求数不为0的时候就会等待。

    电梯线程

    • 本次需要考虑的一个问题是如何避免双轿厢电梯的撞击。我建立了一个Flag类用来表示换乘楼层是否被占用。当换乘楼层被占用并且另一部电梯要到达时,就让另一部电梯等待直到电梯离开。在一部电梯请求完成后,在结束这部电梯线程前,需要进行判断:如果在换乘楼层,就让这个电梯先运行一层,避免一直占用换乘楼层导致另一部电梯运行不了。

      //Flag类
      public synchronized void setOccupied() {
          this.state = 1;
          notifyAll();
      }
      
      public synchronized void setRelease() {
          this.state = 0;
          notifyAll();
      }
      
      synchronized (occupy) {
      if (occupy.getState() == 1 && getNextFloor() == transferFloor) {
      //wait
      }
      //move one floor
      if (nowFloor == transferFloor || anotherCar.getNowFloor() == transferFloor) {
      occupy.setOccupied();
      }
      else { occupy.setRelease(); }
      //设置occupy状态 
      }
      
  • 双轿厢电梯到达换乘楼层时,需要把人全部送出去,重新分配没有到达目的地的乘客。需要对出人的条件再加上一层判断。

在debug时发现一个问题是电梯运行过快,打印输出后发现是因为Accept一个DCreset后电梯所在楼层立即发生改变,但是此时电梯线程还在运行,就会出现跳过某些楼层的现象。这个问题的解决我是采用了meanly状态机的原理,一开始把值赋给nextMAX,nextMoveTime,nextNowFloor然后在reset时把next赋给now(如下代码所示)。这样子就解决了这个问题。

this.max = nextMax;
this.moveTime = nextMoveTime;
if (doubleCarFlag) {
  this.nowFloor = nextNowFloor;
}

线程安全的处理

双轿厢电梯之间共享一个Flag类型的成员变量occupy,在使用occupy去判断是否发生撞击时需要把该成员变量锁住。

debug方法

我主要是看报错类型再加上打印输出。比如CTLE大概率会是轮询,可以在while循环中打印输出,看是哪一部分一直在占用CPU;RTLE可能是出现了死锁。出现其他类型的错误,大概率是线程不安全导致的,需要判断增删某些锁。这一部分比较困难,需要缜密的逻辑。

心得体会

本单元的作业我写的一塌糊涂,性能分又低错的也多。究其原因,主要是我对多线程、同步块等概念不是非常理解,也不知道什么时候用sychronized、notifyAll等;有很多工具也不会使用,导致花了大量时间,事倍功半;逻辑上漏洞百出,细节处理不够全面,经常是拆了东墙补西墙。
总的来说,这单元作业写的非常烂,经常在漫长的debug中度过。但也加深了对多线程的理解,希望接下来的日子能好过一点。

...全文
59 回复 打赏 收藏 转发到动态 举报
写回复
用AI写文章
回复
切换为时间正序
请发表友善的回复…
发表回复

301

社区成员

发帖
与我相关
我的任务
社区描述
2023年北航面向对象设计与构造
学习 高校
社区管理员
  • YannaZhang
  • CajZella
  • C_ecelia
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告
暂无公告

试试用AI创作助手写篇文章吧