OO 第二单元 电梯

樊金鹏-21374235 2024-04-20 00:59:50

OO_Unit_Blog


个人体会

相比于第一单元的作业,这一单元的电梯作业相较之下架需求简单,迭代要求少,基本只要是有着合理的调度算法和对多线程正确的理解,都可以保证高质量的完成三次作业任务,在完成作业的同时,加深了自己对多线程共享资源,互相访问,线程安全,以及读写锁等新概念有了更好的理解。

作业概述 :

本次作业以模拟电梯运行为背景,需求从乘客指定电梯ID到乘客可乘坐任意电梯,最后到实现双轿厢电梯这三个大方向,同时实现了电梯的重置,通过sleep模拟实际电梯运行时间等,采用生产者—消费者模式,电梯作为消费者,输入端作为生产者,乘客作为商品,主要考察练习了学生对对多线程的理解能力,以及对随之而来的多线程的同步锁的选择与设置问题还有不同线程之间的交互问题等。

UML类图:

img

代码复杂度分析:

img

方法复杂度:

img

img

----不难看出较为复杂的方法集中在几个线程类里面,这也正对应该单元作业的目的。

hw5:

  • __作业要求__:本次作业的目标是模拟多线程实时电梯系统,熟悉线程的创建、运行等基本操作,熟悉多线程程序的设计方法,每位乘客都有自己的ID和想要乘坐的电梯ID,只要安全合理的将乘客运送即可。
  • 基本思路:每单元作业第一次架构都十分的痛苦,因为要面对一个崭新的单元从零开始,好在有实验代码和往届博客提示我们从生产者消费者的模式入手,第一次架构代码整体逻辑正是从此入手(_且在这单元第一次研讨课上发现大家貌似都是借鉴了实验代码的架构_),类似于生产者——消费者的模式,设计了输入线程(生产者),调度线程(协调者),电梯线程(消费者),乘客类(商品类),乘客队列类(商品的托盘),策略类(局部协调者)。第一次作业难在架构,需求简单,只有将乘客安全送至目的地的要求,甚至每位乘客都贴心的制定了电梯
    • 输入流: 在这一部分的工作者,严格按照生产者消费者模式,输入流作为生产者,每次读入一个新乘客,并将其按要求分入大盘子中,然后根据乘客想要乘坐的电梯ID将其分入各个电梯线程的小盘子中,等候处理。
    • 调度类: 关于调度类,因为最开始的时候也是看了各种各样的博客,但写着写着越发觉得这个类有些多余,所以在经历了一次重构以后我彻底删除了自己的调度类,而把乘客的调度工作放在了输入流里面,但事后想想这样事实上是违背了所谓“单一性原则”,虽然不会对作业造成任何的影响,但导致了自己代码可读性下降,线程工作不够专门化等问题。
    • __电梯:__:典型的消费者线程,负责处理来自自己小盘子中的乘客请求,而关于这部分电梯策略的问题,得益于指导书里的提示(如下图):

img

我又设计了一个stragety类,我个人感觉可以将其看为“电梯的大脑”,其作为一个对象,可以访问电梯所有的状态例如:当前位置,当前方向,当前等候队列和电梯内部队列,以这些状态为基础,从而得到出正确的行动策略,好让电梯有着下一步的动作,而电梯线程只需要做动作,不需要有它自己的思考,就好像“身体”一样。当然后面在研讨课的时候也听到有同学将策略类作为线程,电梯类作为对象,在听取了同学的分享以后也觉得颇有道理,这两种方式殊途同归,只是我在看了指导书以后自己先入为主的选择了电梯作为线程。

  • __性能优化::本次性能分主要从三个方面入手:__系统整体运行时间, 等待时间与期望时间差的最大值(尽量快速完成所有乘客请求), 还有系统耗电量,最开始的时候我很迷惑,如何才能兼顾这三者呢,但是在后来完成第一次作业进行优化的时候发现,这三者事实上对性能的提升是同向正相关的,只要对其中一个方面作了处理,其他两个颇有水涨船高之感。
    而真正的性能优化来自于每个人的调度策略,我自己是选择了ALS算法,设置了最小化损失函数getvip(strategy中的方法,用于在每次等候队列或电梯内乘客发生变化时更新主请求),同时也了解了大部分同学采用了look算法,相比之下我觉得二者各有优点,look算法也许可以减少乘客等待时间和电梯能耗,但我觉得在系统整体运行时间方面ALS算法要优于look,但正如前文提到的这三者相辅相成,所以最后下来其实无太大的区别。
  • Bug分析: 多线程的并发性从本质上决定了这次作业是难以deBug的,问题主要出现在:线程开始后不可以单步调试,线程互相操作之间先后顺序不一定,线程安全问题带来的死锁问题等。
  • deBug方法:
    在经历开始的折磨以后,我选择的方法是在线程中适当的部分加上输出语句,输出包括但不仅限于:各个托盘中乘客数量,每个乘客的需求是否完成,当前电梯所在位置,当前电梯的方向,当前电梯主请求是谁,以及为了解决线程是否能正常工作后结束而设置的电梯结束信号等,这样以观察各线程的工作情况和对乘客的处理情况,大致上还是能够满足我对debug的整体需求。当然,对于一些隐藏深,复现难,难考测的bug来说,除了多跑几次期待它能大发慈悲再次出现好让我逐句分析以外,我也就只能使用"瞪眼法"来凭空想象到底是什么地方出现问题了(悲伤)。
  • 互测: 第一次作业互测没进去简直是我这一单元最悲伤的一集,究其原因是在于我自己在第一次架构完成以后debug的过程中为了检查每一部电梯的正确性,而把电梯线程的生成从<=6改成了小于等于elevatorQueue.size(),结果就导致了部分乘客要求得不到处理(没想到还能过了中测)。
  • 同步块的设置和锁的选择: 因为在重构以后删去了schedule类,我的第一次作业中就变成了只有两个线程,一个共享对象——即每个电梯自己的小盘子,因此同步块的设置和锁的选择就变得是否简单:输入流在检测到休止符以前不许停止,电梯处理线程检测到自己的小盘子中无乘客时,先判断自己的小盘子是否被setEnd,如果Yes,则电梯线程结束,如果No,则电梯线程wait,wait的锁正是自己的小盘子,而对于电梯盘子waitQueue的类,它的每个方法我都使用了synchronized 修饰词限定,在做完第一次作业的时候我的想法就是能对方法上锁尽量不要对代码块上锁,以避免出现"占着茅坑不那啥"的情况出现,在第一次作业研讨中我也确实听到同学们对于输入线程抢不到托盘的锁的问题各种各样的解决方法,但我采用前文提到的策略好像确实没出现什么太大的问题。

    hw6:

  • 作业要求 : 本次作业的迭代在第一次的作业要求上新增了乘客乘坐电梯指定取消,以及加上了电梯重置这一动作。同时加上了receive要求,保证电梯在合适的情况下才能move。
  • 基本思路:每次做单元作业最痛苦的是第一次,而第二第三次作业在有了第一次作业 正确!正确!正确! 的架构下会简单许多,乘客乘坐电梯指定取消自然带来了性能问题,如何为乘客分配最适合的电梯?而Reset动作完全可以视为电梯的一个新动作,其优先级高于第三次move(因为指导书里明确指出Reset_accept和Reset_begin中可以存在至多两次arrive)。
    • __乘客指定乘坐电梯取消__:
      既然乘客指定乘坐电梯取消,必然需要我们人为的去为乘客分配电梯,可是如何才能在整体性能最优的情况下为乘客指定电梯呢,或者说如何为乘客分配电梯才能实现性能最优呢?在和室友讨论一次次和看了一个个博客之后,最终觉得有两种方法可行:影子电梯和随机分配。影子电梯的主要思路是去深克隆电梯,设计性能计算公式,通过计算比较不同的影子电梯的性能得分,选择得分最高的分配方式将其作用在真实的电梯上从而得到最优解,然而笔者认为这种方式较为复杂且会不会对CPU负荷较大?(然而课上探讨确实有同学实践证明这种写法完全在课程组的考量范围之内,并没有超出CPU计算时间限制,可行且能得到最优解),故而笔者选择了后者方法,随机策略:这种策略的得出在于思考了许多的情景和想了很多的调度策略却发现各种策略始终只能实现局部最优,无法实现全局最优,那不如就采用全随机的方式,舍弃全局最优,但减少出现全局最劣的机会,但我第三次作业证明了,使用影子电梯的同学的性能确实爆了我的随机(悲伤++),但是影子电梯做的极度好的同学也还是只是实现了局部最优(看到许多影子电梯的同学强测点呈现极端情况——有100的点的同时甚至有强测点没过)。
    • __动作Reset的实现__:这个新动作的实现难度适中,笔者的解决方式是为每个电梯赋予了新的小盘子用来装reset请求,且其优先级高于第三次move,设置count在move中,如果将要第三次,则跳出move去实现reset。同时注意到reset的时候要清空电梯内所有乘客,这会带来新的问题,一部电梯貌似可以成为其他电梯(包括自己)新的生产者?据此我在电梯线程的结束条件中加入了"所有电梯是否都结束reset"这一动作,从而保证因为重置而被请出去的客人依旧能完成要求。
    • __receive的实现__:在这里笔者的想法是,每当调度线程(也即我那担任了schedule角色的input线程)分配给电梯线程小盘子一个乘客时就receive,同时注意到reset可以取消receive这一提示,我为自己的乘客类赋予了ifReceived这一属性,在被reset时将其置false以实现取消动作。
  • 性能优化:这里事实上不需要太大的改动,我也考虑过是否要把自己的分配策略换成影子电梯,但出于时间成本的考量,最终还是放弃了这一想法。
  • Bug分析: 得益于random低性能但高防的分配策略,本次作业未出现Bug。
  • 互测: 同上,本次作业未被hack成功。
  • 同步块的设置和锁的选择: 第二次作业好像在这部分并未发生太大改动,无非就是给新写的方法上锁,同时给新写的小盘子reset加了个锁,这个锁主要是用于调度线程判断是否所有电梯线程都已经结束reset和用于电梯自己的reset,deBug时候也没有发现死锁等问题。

    hw7

  • __作业要求__:本次作业在第二次的基础上新增了重置为双轿厢电梯的请求,从而也增加了乘客换乘的请求,以及电梯在换乘层无视receive即可移动的提示。
  • 基本思路: 在Elevator新增一个DoubleCarReset方法,类似于NormalReset方法,而对于换乘层撞车,我的方法是新建共享资源类ChangeFloor,实现move回退。同时注意到,双轿厢电梯有可能随时成为其他任何一部电梯的生产者,所以电梯线程的结束条件也要发生部分改变。
    • __动作DoubleCarReset的实现__:在有了第二次作业的Reset方法基础之后,对于DoubleCar的处理,就是在NormalReset的基础上实现新要求,对于双轿厢重置要求,笔者的做法是新开一个电梯线程,原来的线程作为电梯A(下部电梯),新电梯线程作为电梯B(上部电梯),它们享有共享对象等候队列同时享有共享对象ChangeFloor。

    • __双轿厢防撞车的实现__:在重置为双轿厢电梯的的同时,让A,B线程享有共同操作对象ChangeFloor,在move时特判楼层,如果是换乘楼层,则必须要先获得ChangeFloor的锁才可以继续运行,否则wait,同时因为电梯在换乘层可以无视receive的做法,我干脆让所有的电梯在换乘层完成工作以后实现withdraw动作,下部电梯下一层,上部电梯上一层,同时在withdraw的最后释放ChangeFloor的锁,让AB电梯可以互相唤醒。

      if (pos == changeFloor.getChangeFloor()) {
              try {
                  sleep((int) (moveTime * 1000));
              } catch (InterruptedException e) { e.printStackTrace(); }
              if (aob == "A") {
                  pos--;
                 ...
              } else {
                  pos++;
                 ...
              Floorwork();//回退一格的楼层工作
              ... } 
      if (pos == changeFloor.getChangeFloor()) { //如果到了换乘楼层
              synchronized (changeFloor) {
                 ...
                  changeFloor.notifyAll(); } }
      
    • __电梯线程结束条件改变__:因为任何一部电梯都有可能被重置为双轿厢电梯,而任何一部双轿厢的换乘楼层都是不定的,也就是说,只要一个乘客坐上了电梯,他随时都有可能因为任何一部电梯的换乘而去到其他任何一部电梯,这种感觉笔者第一次就觉得有点像我们reset清人的时候的操作,只不过这种生产将会更加频繁,尤其当所有电梯被重置为双轿厢电梯的时候,所以我选择直接采用最暴力的方式,新建了一个大盘子allPassenger,每当输入线程读入一个乘客请求便将其加入大盘子中,每当有乘客到达自己需求楼层便把他从大盘子中删去,而任何一部电梯的结束条件里加上是否大盘子为空,这样就实现了当且仅当所有乘客要求都完成时,电梯线程才可以结束,以解决新生产的问题。

  • Bug分析:
    被人hack到了(悲伤++)。
    本次Bug出现在没有做好线程安全的问题,因为AB共享同一个waitQueue,相当于之前是waitQueue只对调度线程和电梯线程可见,而对它的所有上锁操作我都是采用了方法,但是现在AB共享一个waitQueue的时候,当A在reset的时候清人对waitQueue操作的时候,可能B要去访问waitQueue,按理来说应该在对A对waitQueue的操作加上锁,让B这个时候不要去访问,等候锁,否则就会偶尔出现线程不安全的问题。
  • 同步块的设置和锁的选择: 这次作业中,出现了线程不安全的问题,主要原因还是在于我在新开B线程的时候没有考虑全面,只是单纯的认为AB完全可以使用同一个等待列表,但是却疏忽了这个等待列表会引起的可能的线程安全问题,虽然出现的概率小,但终究还是有这样的bug可能会出现。

心得体会

打开了多线程新世界的大门。
  • 首先说说我完成这次作业的感受吧,我经常会在第二单元作业写着写着发出感慨“啊OO真的是逼着我前进啊,虽然很不爽,但我真的提升了很多”,是啊,第一次作业我甚至连语言都看不懂,现在已经能自己根据自己想要的东西去实现需求了。
  • 线程安全方面:首先真正的理解了什么叫多线程,然后明白了为什么会出现线程安全的问题,哪里容易出现线程安全的问题:比如竞争冲突和死锁,也感觉积累了一些处理线程安全方面的经验吧,怎么样的写法可能带来线程安全的问题,而面对这些线程安全的问题又有着什么样的方法可以去避免或者解决它们,线程安全对于电梯模拟系统至关重要。在这样一个系统中,可能存在多个线程同时访问和修改共享的数据,比如电梯的状态、乘客的位置等。如果没有有效的线程安全措施,就可能出现数据竞争和不一致的情况,导致系统行为不可预测甚至崩溃。因此,在编写代码时学会使用同步机制,比如锁,来保护共享资源,确保多线程操作的正确性这是十分必要的。
  • 层次设计方面:层次化设计可以帮助我更好地组织和管理代码。在电梯模拟系统中,可以将功能划分成多个层次,elevator和strategy二者手脑一般的关系,不同线程之间到底是平行的关系?还是生产者和消费者的关系,又甚至是存在潜在的生产关系等等,一个好的层次设计可以为设计者带来清晰的框图,也能更好的避免出现某些不易预见的bug。

img

比如当你在架构的时候,脑子里能有这样一张你想要架构的层次设计图,或者当你在需求迭代的时候,能够合理全面的问问自己这里要和哪里进行交互,会出现哪些问题怎么去解决这些问题等,一个好的层次设计是一个好的程序的必要条件。

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

301

社区成员

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

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