BUAA_OO_第二单元总结

最终架构

UML图

这是最终架构的UML图
在这里插入图片描述
三次作业的大致架构均与此相似,在三次作业的迭代过程中,我并没有进行太多架构层面的重构。当input线程收到一个新请求,就把这个请求放到请求队列里(Requestlist),调度器线程(Dispatcher)从这个请求队列中取出请求,并根据策略分配给电梯线程(ElevT)对应的二级调度器(RequestTable),相当于加入了对应电梯的侯乘表类,而电梯运行的策略依靠着向策略类(OrderMaker)申请的策略(Order),策略类决定策略的依据则是电梯线程提供的侯乘表和电梯轿厢内部的请求(GroupIn),还有电梯本身的一些参数,比如电梯运行方向(ifup),电梯所在楼层(floornow)。

三次作业中稳定的内容

我认为三次作业中稳定的内容为“生产者-消费者”模式,线程和输入的整体架构。

三次作业中易变的内容

易变的内容是电梯和分配策略的选择,线程的交互方式,类内部的属性和方法。

第一次作业

UML协作图

在这里插入图片描述

同步块与锁

在本次作业中,我采用的均为对方法上锁的方式,也就是说锁均为对象。在多线程中,最容易出现也是最害怕出现的就是多线程同步的相关问题,所以我们通过上锁的方式保证无论共享对象是以什么样的顺序被访问和修改的,线程所得到的信息和所进行的操作都是正确的。在第一次作业的架构中,如上面的UML协作图所示,InputT不断地取出新的请求,然后加入到Requestlist中所维护的请求队列中,而Dispatcher线程也不断地从Requestlist中拿出请求,所以Requestliat对象是上述两个线程的共享对象,所以Requestliat类中凡是涉及到读取或者改变信息的方法都需要加锁(添加synchronized关键字)以确保结果的准确性。与此类似的还有requestTable,同时被电梯类线程和dispatcher共享,相关的方法均需要上锁。

调度器与策略

在本次作业中,请求分配到那部电梯是规定好的,所以并未涉及到电梯分配问题,电梯策略我参考了往届学长的博客后采用了LOOK算法。
LOOK算法在本次作业中的运用可以简述为:当前楼层可以开门接人或者有人要上电梯就开门,假如当前电梯中仍然有顾客或者运行方向的前方仍然有人准备上电梯就继续运行,假如没有就掉头。最终,假如侯乘表的结束标志为真并且电梯内部也没有顾客,电梯线程结束。

bug与debug

第一次作业中所遇到的bug并不多,在中测、强测、互测中也没有被hack中,也没有hack到其他同学。但是在中测最初几次提交中发现我原先的限制轿厢容量策略有问题。开始时,我只在makeorder中加以判断:
在这里插入图片描述
然后假如电梯收到了开门这一指令,就会把符合条件的(方向与电梯运行方向相同的)乘客全部放入电梯里,并不再判断,但其实电梯可能只能再上一个人,却塞进了5个人,导致bug的出现,于是在进行“开门”这一操作时又添加了判断条件:
在这里插入图片描述
这次作业尚未遇到多线程的典型bug,debug的方式是锁定出错原因相关的代码逐个排查。

第二次作业

UML协作图

在这里插入图片描述
主要增加了电梯与dispatcher的信息交流。

迭代内容

本次迭代大致新添了三项内容:电梯的分配、电梯的重置,receive的输出。

  • 电梯的分配:
    起初计划实现影子电梯,但是最终没能实现,采取了random的方法,生成随机数决定分给哪部电梯。
    • 问题之不能分给正在reset的电梯
      在本次迭代中,我新建了一个类(Manager)来管理六部电梯,如下图所示:
      在这里插入图片描述
      在mian函数中,我就将六部电梯交给了所新建的manager对象,同时这个对象也被dispatcher拥有,manager其实是电梯向上向dispatcher传递信息的中间桥梁。其中有一个方法是查询电梯是否正在reset:
      在这里插入图片描述
      虽然本质上就是对应的电梯在向上传递信息,但是让电梯线程成为dispatcher线程的属性实在令人感到怪异,所以新建了这样一个类。
      回到我们的目的,在dispatcher中,假如生成的随机数对应的电梯正在reset,那么进入到一个循环中:
      在这里插入图片描述
      注意这里我的策略中假如有没在reset的电梯,我采用的是顺序查询(从1至6),有就直接把请求给这个电梯,这个策略其实是有很大缺陷的,为后面互测被hack留下了隐患,在第三次作业的时候我会提到这个bug。
    • 勉强的微小优化
      既然已经写了manager类,那么再用这个类多干点事情就更好了。所以在这个类中还有这个方法:
      在这里插入图片描述
      逻辑大致为假如有电梯在wait就从正在wait的电梯中挑一个。但其实这样的情况也不多见,只能算是勉强的小优化。
  • 电梯的重置
    电梯的重置我仍然利用manager来实现。首先,在电梯中有相应的iftoreset标志位,在电梯获取策略之前,先进行这样的判断:
    在这里插入图片描述
    manager负责向电梯传递充值的数据和设置标志位。所以,manager其实是一个双向的信息通道,横跨在dispatcher和elevt之间。重置操作本身并不复杂,将原先确定的数值改为变量,重置时更改变量的值即可。
  • receive的输出
    普通的请求我均在dispatcher中分配电梯时输出receive。难点在于reset过程中中途出电梯,以及电梯侯乘表中的乘客。这些乘客的原先receive已经被取消,解决方法主要有两个:
    • 将请求更改(中途下去的乘客起始楼层需要更改)后送回原先的requestlist。这种方法最标准,可扩展性好,但是会出现难以结束线程的问题。解决这一问题的方法我参考了一篇往届学长的博客,将在第三次作业中分享。
    • 假装receive取消但其实没有。这种方式简单粗暴,规避线程交互问题,但是可扩展性差。将中途出去的乘客临时存储在侯乘表,等到该电梯reset完成后,将所有乘客全部输出receive,仍然回到该部电梯的侯乘表中。

同步块与锁

在此次作业中,我仍然采用的是对方法加锁的方式,与第一次作业几乎相同。

调度器与策略

我的调度策略即为在上述描述的random加微小优化,从结果上看性能并不好。电梯策略也没有变化,仍然是第一次作业的LOOK算法。

bug与debug

  • 中测
    代码初步完成后,我先运行了一些简单的样例,每个样例尽量只针对一个特定的新增功能,同时在代码相应位置打印信息,比如,遇到线程结束不了的bug,就在输入线程、分配线程、每一个电梯线程结束前增加打印“……is end”,再运行时就能知道是那个地方没结束,然后再具体到“为什么这个线程没有结束呢”,逐步排查其上的每一个地方能否结束。
  • 强测和互测
    这次的强测和互测暴露出的是同一个错误,即input线程收到请求后,不管是什么类型的请求都塞给requestlist,而dispatcher又顺序地从requestlist中拿请求,拿的时候也并不管拿出的是哪种请求,拿出来了才判断是哪一种。这就导致当请求密集地到来,reset请求可能会在队列中排在后面,相关的电梯迟迟等不来reset命令,而是继续运行,导致电梯的移动超过两层。针对这个bug的修复方法应该有很多,比如在requestlist单开一个容器管理reset请求,但是当时考虑尽可能少破坏原先的结构,就直接在requestlist向dispatcher提供请求的那个方法里增加了一个遍历,假如有reset请求就优先处理。

第三次作业

UML协作图

在这里插入图片描述

迭代内容

这次的迭代主要内容有新增重置指令——可以将某一电梯重置为双轿厢电梯,以及相关的receive等规则的调整。重点在于双轿厢电梯的实现。
对于新增的双轿厢reset请求,实现大概分为两部分:

  • reset的“通知”,参数意义上的reset
    这一部分和原先的reset几乎完全一样,通过manager“通知”电梯将要进行“dbreset”,同时将重置后的参数传递给这个电梯。
  • 双轿厢的实现
    我采取的方法是在原先这个电梯线程新开两个线程,然后结束原先的线程。

避免轿厢碰撞的方法

我采取的核心方法是新建一个类”Sy“。这个类完全是空的,只是用作同步块的锁,在原先的电梯线程中,我们新开了两个线程,把同一个Sy对象传递给这两个电梯。
在这里插入图片描述
在ordermaker中,我分别为电梯a、b写了新的策略方法。具体的策略会在后面进一步描述,但大体上只是沿用了原先的LOOK算法。
在电梯a、b中,将会出现原先电梯中没有的一种order:MOVEANDLEAVE:
在这里插入图片描述
命令如其名,就是电梯的“MOVE”假如是要运行到换成楼层就改为这一指令。这种处理方法会面临一个问题,就是两个线程争夺一把锁时,由于不确定性,存在着某个线程频繁地抢到锁的可能。所以可以在策略上避免这一问题,也就是电梯只有当换乘层存在“想要接的顾客”时才会运行到换乘层。就算两个电梯都想运行到换乘层,其中任一电梯抢到锁,完成了接人动作,就不会再想要回到换乘层。

同步块与锁

这次的加锁方式有两种:对方法加锁以及上述提到的同步块加锁。
要加锁的地方就是涉及到共享对象读写的地方,要避免的是线程死锁和线程无法结束。在研讨课上,有一位同学在发言中提到避免线程死锁要在宏观上控制锁的获取顺序,以避免“我拿着我的锁等着你的锁,你拿着你的锁等着我的锁”的情况发生。我在作业中频繁遇到的是线程无法结束的问题,主要有两方面:

  • 由于中途下电梯的乘客要回到requestlist导致的线程无法结束:
    这个问题是通过阅读了一篇往届学长的博客解决的。即新建一个单例模式类,涉及到”接受任务“和“完成任务”的操作都要对这个单例进行操作。最终在input线程中,只有全部任务完成并且没有新任务才下达结束指令,假如没有新任务但是任务还未完成,输入线程等待。
  • 双轿厢电梯中a、b电梯存在“信息差”的问题。
    最初我的a、b电梯策略几乎完全沿用了原来的电梯的LOOK策略,也就是说线程结束的条件是a、b锁“掌管的”那部分楼层侯乘表为空并且侯乘表结束(此时还没采用上面那个单例模式类)。这样就会导致乘客还在高层坐电梯呢,下面的a电梯就结束了,等到乘客到了换乘楼层就只能干等着了(此时的策略还是乘客中途下电梯不扔回原先的请求池)。解决的方案就是采用上述单例模式类,a、b电梯的结束条件均为侯乘表结束。

调度器与策略

这次的电梯分配策略并没有变化,但是由于新的电梯的加入有所改动。
在这里插入图片描述
由于我设计的双轿厢电梯在重构后,乘客下了侯乘表就会被加回requestlist重新分配电梯,假如是某电梯放下的人再加回原先的电梯效率稍高,于是写了这样的策略。
而a、b电梯的策略则是通过在requestTable新增方法实现的。比如原先的判断侯乘表是否为空改为判断a、b电梯范围内的侯乘表是否为空。

bug与debug

中测的一些错误主要来源于题意理解的问题,输出receive与其他输出的顺序需要仔细思考。
在强测和互测中发现的bug也是同一个:
我在分配电梯的时候首先生成一个1到6之中的一个随机数,然后判断这个电梯是不是正在被reset,假如正在reset就从1至6顺序查找有没有不在reset的电梯,如果有就直接分配给这个电梯。同学们非常敏锐地找到了我这种设计的bug,就是假如其余五个电梯都在reset的时候突然来了很多请求,我就会把这些请求统统塞给一个电梯,导致超时。所以我在发现这个电梯reset后分配线程会先等待0.5秒,这样假如有多个请求到来也不会导致全部塞给同一个电梯了。
在这里插入图片描述

心得体会

本单元作业让我对多线程的基本知识和写法有了基本的认识,也体会到了多线程bug的难以预测和复现。在本单元中,我感到自己没有做到线程和类的内部复杂方法的分离,电梯线程里面方法众多,十分复杂。线程安全方面,我认为研讨课的同学说得很对:主要要做到锁申请的顺序要统一,尽量避免锁的嵌套。层次化设计方面,这三次作业迭代的过程中我尽量维护原有架构,就是因为这种自顶向下的输入结构易于理解和debug,当出现bug,就顺着从最顶端的类向下查找。虽然后续迭代存在信息的循环流动,但是代码的层次化仍然是存在的。

  • 8
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

周铭坤-22373193

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值