BUAA_OO_博客作业2——多线程电梯之旅

0 写在前面

终于,我们结束了OO第二个周期的作业,这几次作业也就是我们所早有耳闻的“电梯”作业。

现在就来写关于这几次作业的总结,主要会分析自己的设计方案和总体收获。


1 多线程协同与同步控制(设计方案)

我的这三次作业都采用了多线程的方式来编码。主要是因为作为一名重修生(去年一些意外,放弃了OO),我早就知道电梯最后会演变成多电梯等。

其次就是,我也知道这次作业是为了锻炼我们的多线程编码能力、了解线程同步与安全设置的,所以我也从一开始就采用了多线程的设计。

我的程序主要分为几大部分,并且这几个部分也就是我的几个线程:

  • 输入处理部分:使用给定的接口,处理输入的数据并将处理后的请求放入请求缓冲池中。
  • 调度器部分:从请求缓冲池读取待处理请求,根据情况分给不同的电梯或者做其他处理后分给电梯(最后一次用到)。
  • 电梯部分:每个电梯有自己的待处理请求池,电梯根据自己请求池内的人和自己的运行逻辑进行运行。

以上三个部分在前两次作业中为三线程,第三次作业中有三个电梯线程和前两个线程,共计五个线程。

1.1 多线程协同

这部分主要讲三个部分如何互相协助,完成任务。

我们称输入处理部分为InputHandler,调度器部分为Scheduler,电梯部分为Elevator。以前两次作业为例(第三次仅仅是增加两部电梯,和前两次没有本质区别)进行说明。

1.1.1 InputHandler

我的InputHanler参考了给出的demo,使用while-true循环的方式进行读输入,当读入为null(即没有后续请求)时break退出while循环,并且修改标志位通知其他线程后续将没有请求输入了。

InputHandler通过将读入的请求存入与调度器共用的缓冲池,实现告知调度器请求的目的。每当InputHandler读入一个请求时,将调用“缓冲池.notify”,试图将调度器唤醒,通知调度器有新的请求。

1.1.2 Scheduler

我的Scheduler也使用while-true循环,但是每次分配任务完成后会根据结束标志位判断自己应当结束或者wait(等待后续请求)。因此Scheduler将不会因为盲目while-true占用过多的CPU时间。

当有新的请求进入时,Scheduler将会因为"缓冲池.notify"而醒来,进行任务的分配。

电梯通过“注册”的方式进入Scheduler,即在主线程中new出来的电梯使用Scheduler的方法进行注册,Scheduler内部有一个注册列表,内有所有电梯。

每当Scheduler将某一任务分配给特定的电梯x时,将会调用“x.notify”以只叫醒x电梯。这样也不会叫醒无关电梯而导致CPU时间浪费。

当Scheduler分配所有任务完成时,如果发现InputHandler已经将结束标志位置为true,则自己也会退出while-true循环,并且将自己与电梯沟通的结束标志位置为true,然后遍历注册列表的所有电梯进行唤醒。

通过这样的方式,可以保证所有电梯都能正常接收到Scheduler发出的结束信号,避免了有的电梯无限wait无人叫醒的情况发生。

1.1.3 Elevator

我的Elevator也使用while-true循环,但是每当电梯内没有待执行任务时将会根据Scheduler发出的结束标志进行wait或者退出。因此Elevator也不会因为盲目while-true而占用过多CPU时间。

电梯运行算法主要参考了LOOK算法,LOOK算法是磁盘调度算法的一种,这里我根据磁盘调度的LOOK我模仿了一个电梯调度的LOOK算法;

我采用LOOK算法的原因有以下几条:

  • 时间相对较短,即性能更好
  • 比较符合我们日常生活中电梯的行为
  • 不会出现饥饿显现(即某请求因为调度算法的原因产生长时间等待)

LOOK算法的具体实现就不在这里给出了,总之LOOK是和ALS稍有不同的一种算法,我个人认为LOOK算法在大量请求的情况下和ALS相比有较大的优势,因此采用了LOOK算法。

电梯将会在自己没有待处理任务时进入wait状态,并且会在自己收到任务时被叫醒。

电梯将会在自己没有待处理任务并且收到Scheduler的结束信号时结束,避免无限wait造成程序无法结束。

1.1.4 其他想说的

我的电梯在设计之初就采用了LOOK算法,这不仅仅是因为我早就猜到(知道)后续会需要捎带请求,更是因为我深知傻瓜电梯实际上是没有意义的,真正的电梯必须有自己的效率。

并且如果我第一次就写好LOOK算法的话后面就可以摸鱼了!

因此我的第五次作业就已经是LOOK算法的电梯了。拜此所赐,我第一周de了不少bug,但是后两周(尤其是第六次作业的时候)我确实轻松了不少。别的不少同学都需要重新思考自己的数据组织,而我除了加个ARRIVE几乎什么都不用干!

1.2 多线程同步(控制)

多线程程序面临的最大的问题之一就是线程同步。所谓线程同步就是所有线程对于共同的访问资源,需要同步信息。在多线程程序中,经常出现read-and-write等操作,因此需要注意共享资源的访问控制。

Java提供了synchronized关键字以实现简单的线程同步。我的程序中使用并且也只使用了synchronized关键字来实现这部分的需求。

在我的程序中,有两部分线程共享资源,一个是初始请求池,另一个是电梯待处理请求池。初始请求池可以由Scheduler和InputHandler操作,分别有读和写需求。

因此我在他们访问这一资源时首先使用synchronized关键字“锁住”这一资源,再对资源进行读写操作。

在电梯的待处理请求池的处理上,我也采用了类似的方式,即封装好的方法内部有synchronized关键字锁住的操作对象,以此保证线程安全。

1.3 自己的思考

在线程同步的学习中,关于需要考虑线程安全的地方,我自己总结了一下几个关键点:

  • 不同线程访问(修改)同一共享资源
  • 对于这次访问有迫切的绝对需要获得正确状态的需要

对于第一点,我们都可以很好地理解。但是对于第二点,我有自己的一些想法:

假设我们有两个人使用同一账户存取款,我们不妨设为A和B;A的操作是向账户里存入100元,B的操作只是读取账户内现有金额,并且在金额大于500时去吃饭。

根据上述设定,A线程的操作是不停向账户内存入100元,每存入一次休息一会;B线程的操作是不停访问账户,当金额大于500时去吃饭。

假设B对于去吃饭并没有迫切的需要,也就是说B并不会因为某次该读501时读到401而饿死,那么我认为B在访问账户内容的时候其实是不必上锁的。

也就是说,假设我们的线程对于某个共享资源的状态没有迫切的需要(即不是特别在意实时反应的那种),只是“日常访问”的话,那么其实对于读操作来说没有必要必须锁住共享对象。


2 基于度量分析自己程序的结构

在这部分中,我们将第5、6、7次作业分别讨论。(长图警告!(也可能是长列表))

注:

  • ev(G)  Essentail Complexity
  • iv(G)   Design Complexity
  • v(G)    圈复杂度

2.1 第五次作业——傻瓜电梯(我的是LOOK电梯)

2.1.1 复杂度分析

 

总体来看在几个比较大的方法的复杂度还是偏高的,值得反思,是否可以拆分为更多的方法?

2.1.2 类图

类图中可以看出,主要有三大线程,和Main线程。Main负责完成启动其余线程的工作。

StopFlag和Person是为了方便,自己封装的两个类。(StopFlag就是结束信号,Person是电梯内的人)

 2.1.3 SOLID原则

SRP单一责任原则:实现较好

OCP开放封闭原则:实现较好

LSP历史替换原则:由于没有继承,不考虑

ISP接口分离原则:由于没有使用除Runnalbe以外的接口,不考虑

DIP依赖倒置原则:实现较好

2.2 第六次作业——ALS电梯(我的还是LOOK电梯)

2.2.1 复杂度分析

 

2.2.2 类图

2.2.3 SOLID原则

SRP单一责任原则:实现较好

OCP开放封闭原则:实现较好

LSP历史替换原则:由于没有继承,不考虑

ISP接口分离原则:由于没有使用除Runnalbe以外的接口,不考虑

DIP依赖倒置原则:实现较好

2.3 第七次作业——蛇皮电梯x3(我的是LOOK电梯x3)

2.3.1 复杂度分析

2.3.2 类图

2.3.3 SOLID原则

SRP单一责任原则:实现较好

OCP开放封闭原则:实现较好

LSP历史替换原则:由于没有继承,不考虑

ISP接口分离原则:由于没有使用除Runnalbe以外的接口,不考虑

DIP依赖倒置原则:实现较好

2.4 总结

实际上,我的三次作业的类就没有变过。所以三次的类图和SOLID分析是同样的。

主要是内部的方法实现发生了改变。不过实际上应该通过继承的方法来实现新的设计。


3 分析自己程序的bug

实际上,我的程序bug基本都在自测阶段处理掉了。

不过,在强测和互测阶段出现过一个超时bug,原因是因为我的电梯在转向的过程中处理状态发生了问题(第二次电梯作业我做了少许修改,结果导致了这个bug)。

不过还是有很多已经被我私下处理掉的bug需要注意的,主要有一下几种:

  • 过早停止的电梯拒载,解决方案:设置StopFlag和合适的算法,使电梯在执行完所有任务时才会停止;
  • 电梯无限wait无法停止,解决方案:在Scheduler结束时叫醒所有电梯,防止电梯wait但是Scheduler停止,无人叫醒电梯导致bug;
  • 早期实现中,我使用暴力轮训导致CPU超时,解决方案:使用notify-wait方式防止无用线程无限被调用,减少CPU时间

 

4 分析自己发现别人bug采用的策略

实际上,我这几次作业并没有认真hack别人的电梯。一方面由于自己有很多其他事情要干,活跃度就交给roommate吧!另一方面是因为我真的不想hack了。

实际上我和别的同学交流过程中发现,不少bug都是由于线程安全导致的。对于此类bug,我们需要着重观察对方程序对于共享对象访问的处理。

另外还有一种bug就属于真正的程序逻辑bug,此类bug大多因为程序员设计漏洞导致。对于这种bug,我们需要真正理解对方想要实现的算法,并且把自己的思考和对方的实现进行对比来发现。


5 心得体会

经历了电梯系列作业的洗礼,我们都有不少收获。

先说关于多线程编程调试的问题,多线程程序难就难在难以调试和复现bug。在这样的情况下更需要我们使用清晰的程序逻辑,方便自己通过“走查”的方式分析自己的bug。

多线程程序另一个重要的问题就是线程安全问题。我认为我们在做设计的时候有两种思路:

  1. 使用自己封装(别人封装)的线程安全类
  2. 在程序中注意线程安全,使用synchronized关键字等

我认为第一种方式是一种比较稳妥的方式,虽然我实际上是使用的第二种方法。事后我也考虑的自己的设计进行了反思。第一种确实是优于第二种的。

因为第一种线程安全类在使用的时候不是需要时刻注意的,并且是一种一劳永逸的方式。但是第二种在你编写新的部分的时候,必须也考虑其他线程是否会使用。

在设计原则上,我们应该坚持SOLID原则,因为这样的程序是易于维护、扩展的。另外我们应当善用单例模式、观察者模式、工厂模式等。以上几种模式都是为了更好地扩展维护的。

以上

猜你喜欢

转载自www.cnblogs.com/heyedan/p/10742265.html