BUAA_OO_Unit2 总结

1.锁与同步块

1.1 hw5

本次作业的主体采用生产者-消费者模型,同步块的设置集中在模型的托盘类,即候乘表中。候乘表中接收输入线程(生产者)产生的请求,而电梯线程(消费者)会对候乘表进行判断以及提取请求,这样就存在读写冲突的可能性,因此给候乘表的相应方法加上同步控制块,以对象本身为锁来达到同步互斥的目的。

1.2 hw6

在本次作业中,输入请求不再指定电梯,需要进行分配,因此我为每一个电梯线程配备独自的已分配待处理请求表,这一模式同样是生产者-消费者,同步控制块的设置与候乘表中的同步控制块的设置类似。

1.3 hw7

在本次作业中,新增双轿电梯,因此需要解决换乘楼层冲突的问题,我新增了TransferLock类,用其作为换乘楼层的锁,保证A、B轿厢不会同时进入换乘楼层。

2.架构与策略分析

2.1 三次作业类图与协作图

类图
在这里插入图片描述

hw5协作图

hw6协作图
在这里插入图片描述
hw7协作图
在这里插入图片描述

2.2 架构变化

2.2.1 hw5—>hw6

hw6相对于hw5:

  • 乘客请求不指定电梯,需要我们自己设计分配方式,这一点只需在Schdule类中新增分配方法即可;
  • 新增了reset指令,这一指令使得每个电梯线程也可以成为生产者,类图中WaitList新增一个来源;
2.2.2 hw6—>hw7

hw7相对于hw6:

  • 出现双轿电梯,针对这一变化,我为每个电梯线程设计了一个elevatorThreadCompany,在收到DCEReset指令后,讲原线程更改为DCEElevator的A轿厢,同时将elevatorThreadCompany作为B轿厢,从而实现双轿电梯;
2.2.3 架构变化小结

在这几次错也迭代中,代码的主体部分始终保持稳定,即生产者-消费者模式:

  • InputThread作为生产者;
  • WaitList作为托盘;
  • Schedule作为消费者;

变化主要集中在针对新增指令的处理以及新增电梯类型的实现,例如这三次作业我都采用了不同的调度策略,以及对于双轿电梯和普通电梯我设计了不同的策略方法。

2.3 调度器与策略分析

2.3.1 调度器

三次作业中我都采用Schedule作为调度器,三次作业中,调度的变化围绕在两方面:

  • 结束条件:在hw5中,只需InputThread结束,那么就可以给各个线程包括Schedule设置结束标志,而在hw6和hw7中新增了reset指令,每个电梯线程也可往WaitList放回请求,因此结束条件需要新增一条,即reset指令全部处理完毕;
  • 分配方法:hw5中请求指定了电梯,因此必须按照要求分配给指定电梯,而在hw6和hw7中需要自己设计分配方法,在hw6中,我采用影子电梯策略,模拟每个电梯的运行,得到局部最优的分配选择;在hw7中,我采用平均分配+调参的分配方法。

调度器通过共享对象,即每个电梯线程的processingList,来实现与电梯线程的交互,调度器可以往共享对象中放入请求,而电梯线程可以从processingList中取出请求,达到两者交互的效果。

2.3.2 策略

在三次作业中,每个电梯的运行调度策略我都采用look策略,不同的地方在于将请求分配给哪个电梯。
在hw6中,我采用影子电梯策略,即对六个电梯线程进行深克隆构造影子电梯,将某一个请求分别放入六个影子电梯中进行模拟运行,得到模拟运行的时间,并选择运行时间最少的电梯将请求放入,从而达到局部最优效果。
在hw7中,因为双轿电梯本身是个双线程,而我没有弄明白如何实现对双线程的模拟运行,所有hw7转为采用更保守的平均分配+调参的分配方法,这一方法实现起来较为简单,只需记录请求的编号,模6进行分配即可。

3.双轿箱的实现

在第三次作业引入双轿电梯,两个电梯轿箱有公共的楼层,即换乘楼层,因此需要实现锁,来保证两个电梯轿箱不会在换乘楼层相撞。
我新增了TransferLock类,类的具体实现如下:
在这里插入图片描述
每个双轿电梯的两个轿厢线程都会共享一个TransferLock实例化对象,以该对象作为换乘楼层的锁。每次有轿厢需要进入换乘楼层,都会调用该锁,判断换乘楼层是否被占用,若是,则等待;否则,进入换乘楼层并将isOccupied设置为True。之后该轿箱离开换乘楼层后再将isOccupied设置为False,通过这一机制实现双轿厢的两个轿厢不碰撞。

4.bug和debug方法

4.1 遇到的bug

  • 在本次作业中,我出现的最多的bug就是有关于设置线程结束的判断条件:
    在hw5中,这一条件较为简单,只需InputThread线程结束,同时WaitList为空,就可以将所有线程的结束标志符设置为True,但是在hw6和hw7中新增了reset指令,使得已分配的指令可以被放回WaitList中,因此结束条件的判断需要同时关注每个电梯线程的reset指令是否都处理完毕。
    而针对设置线程结束的判断条件这一点,在研讨课听取了其他同学的分享,其实有着更好的实现思路。上文中我提到的实现需要同时判断多个不同线程里面的多个属性是否满足要求,这样就必须让这些线程之间存在数据共享,大大增加了耦合度。在研讨课中,有同学分享了一个全新的思路,即通过单例模式,记录输入的请求数量和已经处理完的请求数量,只有当这两者相等的时候我们才将各个线程的结束标志设置为True,这样大大简化了判断逻辑,只关心输入的请求数已处理的请求数,实现起来简单,极大地减少了bug出现的可能性。
  • 陷入死循环:
    在hw6中,我多次出现陷入死循环的bug,经过艰难地问题查找之后,发现是出现在影子电梯的实现中。在正常电梯中,电梯被建立时的初始状态一定是CLOSE,我基于这一前提设计了电梯的运行方法,但在影子电梯的实现中,因为我们需要深克隆电梯此时的真实状态,因此影子电梯被创建的初态可能就是OPEN,这样就导致之前设计的运行方法出现死循环。

4.2 debug方法

在这一单元的作业中,对我帮助最大的方法就是打印输出法
上文中我提到了我多次出现陷入死循环的bug,这一bug的修改其实非常简单,难点在于如何在多个不同的线程、以及每个线程又具有多个任务的背景下准确定位到bug所在位置,而我就是借助打印输出法

  1. 首先我在每个线程的进入以及退出时都进行了打印,首先定位bug出现的线程,即调度线程Schedule
  2. 之后我在Schedulerun方法的每一个分支判断中都加上了进入以及退出的打印,将bug继续精确到了影子电梯的实现中;
  3. 然后,我在影子电梯的类中的电梯运行逻辑的每个分支都加入了打印输出,发现程序陷入死循环后会重复打印“OPEN”,将bug进一步精确到了OPEN操作分支,然后对这一方法进行检查,最终找出问题。

5.心得体会

5.1 线程安全

在本单元作业中,我们第一次接触到了多线程设计,其中线程安全问题是一个非常重要的问题。在我看来,线程安全设计是否准确取决于对线程之间交互的理解,因为线程安全问题大多出现在多个线程同时具有对某共享对象的操作。例如在本单元的作业中的生产者-消费者模型,其中托盘类WaitList就是输入线程与调度线程交互的媒介,我们需要准确把握共享对象的哪些方法存在线程安全问题,例如向WaitList中放入请求和从中取出请求就涉及到读写冲突问题,我们必须全面综合地分析,找到所有的存在线程安全的情况,针对所有的这些情况利用同步控制块和锁来解决冲突问题,这样才能达到线程安全的目的。

5.2 层次化设计

在多线程问题中,层次化设计的重要性更为突出,因为多线程存在线程安全问题,而解决线程安全问题的关键在于梳理清楚各个线程之间的交互关系。通过严格的层次化设计,可以让我们准确把握各个线程之间的交互关系,准确、全面的考虑到所有可能的线程安全问题,最终帮助我们实现多线程的线程安全。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值