OO第二单元心得感想

第二单元博客

目录

一、同步块与锁

1.1 同时读/写

1.2 输出

1.3 流水线架构模式

二、调度器设计

2.1 第五次作业

2.2 第六次作业

2.3 第七次作业

三、架构模式

3.1 设计模式

3.1.1 生产者-消费者模式

3.1.2 master-slave模式

3.1.3 流水线模式

3.1.4 单例模式

3.2 三次作业架构设计的逐步变化

3.2.1 第五次作业

3.2.2 第六次作业

3.2.3 第七次作业

3.3 线程之间的协作关系

3.2.1 第五次作业

3.2.2 第六次作业

3.2.3 第七次作业

四、bug分析

4.1 第五次作业

4.2 第六次作业

4.3 第七次作业

五、心得体会

 


 

一、同步块与锁

在本单元的作业中,有三个地方需要上锁。

1.1 同时读/写

我选择将请求分配到电梯,然后电梯去处理请求的方法来处理请求。因此,对于每部电梯,它的待乘队列processingQueue都面临着读写同时进行的可能。所以,所有涉及请求队列的处理,无论读写,都必须用synchronized关键字去给方法上锁。因此,在三次作业中,对于电梯Elevator类,所有涉及processingQueue修改的方法都要求用synchronized关键字修饰。对于请求队列RequestQueue类,所有涉及修改的方法都要求用synchronized关键字修饰。

1.2 输出

本单元官方包提供的TimableOutput.println()方法是线程不安全的,因此需要用一个类对其进行包装并用synchronized关键字修饰。

1.3 流水线架构模式

在流水线架构模式中,对于结束的判断不能仅仅以InputThread(输入线程)中读取到null请求就判断结束,而是需要在输入结束的同时所有请求都完成了才能判断结束。因此,在输入结束时,需要唤醒所有等待状态的电梯,告知这些电梯该结束了。在唤醒的过程中,需要对每个电梯的队列进行上锁处理。

二、调度器设计

2.1 第五次作业

第五次作业中,由于没有换乘、没有横向电梯、每座仅有一个电梯,所以调度器的设计极其简单,只需要将每层的请求分配到每层对应的电梯中即可。

2.2 第六次作业

第六次作业中,相较第五次作业多了两个点:多了横向电梯、每座/层不止一个电梯。我没有选择让电梯自由竞争,而是选择由主调度器进行分配。具体的分配策略其实也比较简单:对于每个请求,考虑所有能够将ta送到目的地的电梯,哪个电梯人少就分配给哪个电梯处理。

2.3 第七次作业

第七次作业,相较第六次作业需要额外考虑的问题主要有:横向电梯的停靠问题、换乘。对于横向电梯,在分配时要考虑的条件不仅是所在楼层,还有横向电梯能否在待处理横向请求的起点/终点停靠。除此之外,只需要根据当前请求要进行的方向选择电梯,分配方式与第六次相同:哪个电梯人少,就分配给哪个电梯。对于换乘,我选择只考虑一次换乘,简化条件。所以对于每个请求,最多只可能由三步完成:在起点座坐纵向电梯去到中转楼层、在中转楼层坐横向电梯去到目标座,在目标座由中转楼层前往目标楼层。对于目标楼层的确定,选择目标楼层的策略就和官方给出的公式一样:设请求为FROM-P-X-TO-Q-Y,M为横向电梯的停靠信息,m为横向电梯所在楼层,则m为满足(((M >> (P - ‘A’)) & 1) == 1 && ((M >> (Q - ‘A’)) & 1) == 1)且使得|X - m| + |Y - m|最小的值。同时在确定了目标楼层后,每个请求都可以用三位数字(flag)来表示,用0/1表示是否需要这一步,例如100(只需乘坐一次纵向电梯,如FROM-A-5-TO-A-8),010(只需乘坐一次横向电梯,如FROM-A-5-TO-C-5),110(先乘坐一次纵向电梯,再乘坐一次横向电梯,如FROM-A-5-TO-C-1,中转楼层为1层),011(先乘坐一次横向电梯,再乘坐一次纵向电梯,如FROM-A-1-TO-C-5,中转楼层为1层),111(先乘坐一次纵向电梯,再乘坐一次横向电梯,最后乘坐一次纵向电梯,如FROM-A-3-TO-C-5,中转楼层为1),000就表示当前请求已经到达最终目的地。这个表示方法可以通过最高位来确定当前该请求是视为纵向请求还是横向请求,调度器就可以靠这个信息进行分配。

三、架构模式

3.1 设计模式

在本单元的作业中,主要用到了三种模式:生产者-消费者模式,master-slave模式,流水线模式。

3.1.1 生产者-消费者模式

在调度器和其他线程通信都是采用生产者-消费者模型。输入线程-调度器的通信中,调度器是消费者;在调度器-单个电梯的通信中,调度器是生产者。

3.1.2 master-slave模式

由于调度器要同时管理多个电梯,所以调度器-多个电梯又是master-slave模型,调度器是master,可以“看到”所有slave并给它们分配乘客;多个电梯每个都是slave,它只能看到它的上级master、不能看到其他的slave,运行结束时向调度器汇报。

3.1.3 流水线模式

由于第三次作业中乘客需要换乘,当前下电梯的乘客可能并未到达目的地,还需放回,因此调度器-单个电梯又是流水线模式中的Controller-Worker,调度器是Controller,是流水线中的“传送带”,流水线上每个worker干完了一步就放回传送带,由它送往下一步;电梯是Worker,worker干完了它这一步(即运输完一个乘客的当前请求)后判断一下此乘客是否已经到达最终目的地(flag是否等于0),如果没有到达就再将它放回传送带上,即放回调度器的请求队列。

3.1.4 单例模式

确切的说,这个模式只是流水线模式中必须采用的模式——因为流水线的调度器(Controller)只有一个。

3.2 三次作业架构设计的逐步变化

3.2.1 第五次作业

 

MainClass: 主函数,new所有线程并让所有线程启动。

InputThread: 输入线程,将输入的请求丢给调度器。

Schedule: 调度器,把每个请求分配给每个电梯。

Elevator: 电梯类,负责处理每个请求。

Request: 请求类,负责描述每个请求并用get()方法得到其信息。

RequestQueue: 请求队列类,负责存储请求,在第五次作业中有两种用途:Schedule(调度器)的待分配队列waitQueue,每个电梯的待处理队列processingQueue,对于两种用途在RequestQueue类中都有方法来处理。

SafeOutput: 如前文中提到的,将TimableOutput.println()方法包装,使其线程安全。

3.2.2 第六次作业

 

 

第六次作业相较第五次作业,进行的处理只是将Elevator类分成了ElevatorBuliding类(纵向电梯)、ElevatorFloor类(横向电梯)。同时在InputThread类中增加了请求判断(判断是乘客请求还是添加电梯请求)、电梯增加(判断是添加电梯请求后直接在InputThread类处理,减轻Schedule类压力),在Schedule类按前面所述的分配策略进行了修改。顺便,由于官方包中用Request表示请求,所以对于乘客请求由Request更名为MyRequest。

3.2.3 第七次作业

 

 

第七次作业,由于需要更换成流水线模式,所以对于程序整体修改较大,但核心没有变化。

MainClass: 主函数,对于Schedule类进行初始化,功能修改为只启动InputThread线程。

InputThread: 如前文所讲,增加了判断所有请求结束后给Schedule状态设为结束(EndTag)。

MyRequest: 增加了一个属性tempFloor,表示该请求的中转楼层。

RequestCounter: 每个请求结束后给这个类一个信号,这个类负责判断所有请求的结束。所有请求结束后,这个类才会从等待状态中唤醒。

3.3 线程之间的协作关系

3.2.1 第五次作业

 

 

3.2.2 第六次作业

 

 

3.2.3 第七次作业

 

 

四、bug分析

4.1 第五次作业

第五次作业我遇到的主要问题集中在三方面。

第一,一开始我选择在实验代码的基础上进行修改开发,但是对实验代码没能完全理解,因此结束条件设置错误,导致出现CTLE。

第二,我一开始选择的策略是相对简单粗暴的Scan算法,选择每次都跑到头在转头判断。这种策略虽然实现极其简单,但是时间性能相较Look算法(或ALS算法)差的还是比较多,最终在强测RTLE。所以在第五次作业的bug修复中,我将程序改为Look算法,最终性能算是过了强测。

第三,就是输出的安全性。在最初我没有选择将官方包的println()方法进行包装,导致输出时线程不安全。

4.2 第六次作业

第六次作业我的bug相对比较蠢。如上文所述,多部电梯的分配策略是哪部电梯人少就将乘客分配给某座电梯。我的bug出现在比较最小值时初始值设置过小导致出现问题。

4.3 第七次作业

第七次作业我的bug出现在横向电梯的停靠上。我只考虑了乘客能够在终点座下电梯,忘记考虑乘客能否在起点座上电梯。

五、心得体会

在完成第二单元的代码过程中,我体会到许多。

第一,调度策略与性能优化。在第五次作业中,我一开始选择简单粗暴地实现Scan策略,但是经过强测数据后发现选择这样的策略会导致电梯在部分时间无意义运行,从而导致时间超过数据的最大运行时间Tmax。在将Scan策略换成Look策略过后,可以明显地感觉到程序无意义运行时间减少,整体性能提升。

第二,线程安全。在多线程运行的过程中,很多时候会出现对于某个对象,一边有写入的需求,另一边有读取的需求。如果不加以上锁使其原子化,将会出现非常严重的后果。输出的时间戳非递增也是线程不安全的体现。

posted @ 2022-05-02 18:21  普通桑  阅读(8)  评论(0编辑  收藏  举报