OO第二单元总结

 

一、同步块的设置和锁的选择

设置同步块其实只需要搞清楚哪些资源需要共享即可。我在三次作业设置的共享资源具有同一性,均为输入类Input与调度器类Schedule之间共享的待调度请求队列waitQueueSchedule类与电梯类Elevator之间的待处理队列processingQueue以及整个电梯系统共享的输出类Output

为避免线程安全问题,我对上述三个共享资源的方法都加了锁,保证同一时间内只有一个线程访问加了锁的对象。而为了避免轮询,我采用了wait-notifyall的策略,其在各个作业中是如何实现的我将在第三部分详细展开。

二、调度器设置

在我的程序里,调度器起到的作用是接受来自输入类的请求,对请求进行分析,把他送往相应的电梯处理队列中,而其具体实现在三次作业里均有不同点。

hw5:由于第五次作业只有纵向电梯,请求的移动均为纵向且能一次实现,因而调度器的输入来源只有Input类,根据请求出发地所在楼座送往相应的processingQueue,是十分简单的生产者-消费者模型。同时在Input结束时,向Schedule发出结束信号,Schedule判断可以结束后再向各个电梯发出结束信号。

hw6:在第五次作业的基础上增加了横向电梯,调度器需要增加能够识别处理横向请求的能力,但由于请求的出发地和目的地要么楼座相同,要么楼层相同,请求只需一次调度便可实现。使得Schedule类的输入和输出来源与hw5基本一致,只是输出来源在细分为横向和纵向处理队列的区别。

hw7:第七次作业中请求的出发地和目的地的楼座和楼层均可不相同,请求的完成需要进行“换乘”操作。因此我在Input类里对请求进行了分解操作,把大请求分解为具有和hw6请求相同特性的N个小请求。因此调度的识别分配功能仍与hw6保持一致,但当一个大请求中的一个小请求在Elevator类里实现后,若还有小请求未处理完,需要回到Schedule类里再次接受调度,就便使得调度器的输入来源不只是Input类,还有Elevator类。在结束时调度器的结束判断条件会和前两次作业有些不同。

三、三次作业架构迭代和协作

第一次作业:

UML类图和UML时序图如下:

 

 

架构说明:

第一次作业中,我的架构里面主要有三种类型的线程,分别为输入类Input、调度器类Schedule以及电梯类Elevator,其中每个楼座的电梯都各自享有一个待处理队列processingQueue,因为有5个电梯,那么就有5processingQueue,在InputSchedule之间有个待调度队列waitQueue。当一个请求有Input接收后,输入到waitQueue,再由Schedule接受,Schedule对请求进行解析,把请求分配往相应的processingQueue队列中。

电梯的具体调度策略我采用了LOOK算法,原则是以电梯内及带处理队列中同方向的请求中,目的地最远的请求为主请求,在运输过程中捎带同方向的请求,因此能保证电梯在一次往返中尽可能多地处理请求。若电梯内部无请求,则电梯会以待处理队列里的第一个请求作为主请求。

同时我的电梯采用了决策和运行分离的做法。当电梯要进行下一步操作时,先向决策类提供电梯实时信息,有决策类根据电梯信息以及processingQueue状态做出下一步决策,并将其反馈给电梯类,电梯根据决策做出行为。这种做法使得整个电梯类具有很好的可扩展性,在之后的作业中若需增添新功能仅需更换决策即可。

避免轮询的方法:在这次作业中比较容易出现轮询的两种场景分别为:

1waitQueue为空,但Schedule仍反复访问waitQueue想获取待调度的请求。解决方法其实是若无待调度的请求,则让Schedule进入wait状态,当Input输入一个新请求或是输入结束信号时,进行notifyall操作,唤醒Schedule

2processingQueue为空,但Elevator仍反复访问processingQueue想获取待处理的请求。解决方法同(1)。

第二次作业:

UML类图和UML时序图如下:

 

 

 

 

架构分析:

在第二次作业中电梯系统增加了增加电梯和横向运输乘客的功能。其中增加电梯的请求我在Input类里实现,当接收到该类型的请求时我便新开一个电梯线程,而其中会出现一个楼座会有两个及以上的电梯,因此我在同座或同层电梯之间采用自由竞争的策略,多个电梯共享一个processingQueue,每个电梯去寻找适合自己处理的请求,谁最先抢到谁就先处理。

横向电梯的调度策略不同于纵向电梯,因为横向电梯是环形电梯,周而复始,没有尽头,若采用LOOK算法会出现难以掉头的情况,因而我采用ALS算法,不考虑待处理队列中的请求,仅考虑将电梯内部的请求作为主请求,提高电梯的掉头频率。

而原电梯类Elevator我分为了横向电梯类TraElevator和纵向电梯类ProElevator,分别处理横向请求和纵向请求,但内部具体实现功能及运行机制和hw5差不多,就不细说了。

第三次作业:

UML类图和UML时序图如下:

 

 

 

架构分析:

第三次作业和前两次作业最大的不同应该就是电梯的“换乘”处理以及掩码的使用了,其他不同均只需在小地方改动即可。

虽然掩码看起来高大上,但由于课程组已把掩码使用公式给出,就算不会也只要照着公式使用即可。主要是“换乘”问题的解决具有多样性。而我采用的是最为保守的“静态分解”法,即在Input接受请求时便对请求进行分解,把“(atFloor == toFloor) + (atBuilding == toBuilding) == 0”的大请求分解成N个“(atFloor == toFloor) + (atBuilding == toBuilding) != 0”的小请求,且具有先后顺序,这样请求的处理机制便和第二次作业一致了。当一个大请求的一个小请求完成,若还存在小请求,大请求需返回调度器重新进行调度。

在线程结束机制方面与前两次作业略有不同,前两次作业中Schedule判断可以结束的标志是接收到来自Input的结束信号,以及waitQueue里已无待调度请求。这次作业由于调度器的输入来源新增了电梯类,还需判断电梯之后是否还存在需要调度的请求,因此我设立了两个变量preNumfinishNum来统计进入电梯系统的大请求数量和已处理的大请求数量,只有二者相等,Schedule方可结束。

横向电梯和纵向电梯的调度策略和前一次作业保持一致。

四、分析自身程序中的Bug

hw5

在第一次作业中我强测未出现bug,互测出现了输出线程不安全的bug导致被hack。解决方法是新增一个共享输出类Output,输出方法加锁,保证每次只有一个线程调用该方法,保证线程安全性。

hw6 & hw7

后两次作业的强测和互测均未发现bug

五、分析自己发现别人Bug时采用的策略

1)检查输出线程是否安全。在同一时间制造多个不同楼座的请求,看其输出的时间戳是否保证单调不减性。

2)检查各种类型的请求处理情况。同楼层、同楼座、不同楼层楼座的请求处理情况。

3)检查电梯之间竞争是否会导致死锁或轮询问题,在含有多个电梯的楼座或楼层加入大量随机数据。

4)电梯的调度策略,这个主要观察代码分析+制造数据分析,寻找调度策略的漏洞。

六、新的体会

说实话,相比于第一单元拆东墙补西墙的状态,这一单元明显要从容得多,我觉得这主要归功于第一次作业的架构设计的好,每个类分工都比较明确,使得后两次作业基本不需要进行大幅度的改动,并且思路和逻辑也清晰多了。

对于线程也有了更加深入的理解,确实很多问题因为多线程的引入速度快了不少。而在线程之间的协作方面要十分清楚哪些资源共享,哪些是写操作,哪些是读操作,wait-notifyall的设计要清晰,必要时才进行notifyall操作,避免无效唤醒。

debug方面也精进不少,掌握了多线程debug的方法,对于轮询问题可以用printf法找到反复调用的代码段,死锁问题用调试的方法解决。

以上便是我的些许收获。

 

posted @ 2022-05-02 17:08  鹏程万里orz  阅读(33)  评论(0编辑  收藏  举报