第二单元总结

第二单元总结

一、三次作业架构设计

1、第一次作业

 

第一次作业需要实现每个楼座只有一部纵向电梯的调度与运输,因此我采用了生产者-消费者模型,将输入的请求放入等候队列中,由调度线程根据请求到达的早晚来将请求分配给不同的电梯,而电梯则在运行过程中根据ALS算法判断应该在什么时候开关门以及运送哪些乘客。其中等候队列是输入线程与调度线程之间的”托盘“,输入线程作为生产者可以按时放入请求,而一旦有请求放入“托盘”,调度线程作为消费者则会将等候队列中的请求分发到各部电梯的候乘表中,这是第一层生产者-消费者模型。而对每一部电梯来说,同样应用了生产者-消费者模型——只不过这时调度线程变成了生产者,每一步电梯的候乘表是“托盘”,而每一部电梯则成为了消费者,负责将候乘表中的请求运送到目的地。感觉在调度时直接将请求分配给不同的电梯的做法可以避免电梯因为争夺请求而产生不必要的移动,在完成代码时也比较简洁。

2、第二次作业

 

第二次作业增加了横向电梯的电梯类型,同时在请求中增加了增加电梯这一请求种类。因此我在第五次作业的基础上在电梯处理请求的Process类中仿照处理纵向电梯运送请求的方式,增加了处理横向电梯运送请求的几个方法。然后主要调整了调度线程的实现——由于我的电梯队列是由Schedule类进行管理的,因此我在之前的基础上将电梯队列扩充为两个,分别是横向电梯队列与纵向电梯队列,因此在分发请求时可以先判断是增加电梯请求还是乘客运输请求,若为增加电梯请求则直接在相应电梯队列中增加一部电梯,并启动该线程;若为乘客运输请求则根据是横向运输或纵向运输,在相应的电梯队列中查找请求对应的电梯。同时,在这次作业实现过程中为了保证输出安全,又新增了Printer类,使得输出操作具有原子性。

3、第三次作业

 

第三次作业增加了换乘请求,同时新增电梯的容量、速度、开关楼层/楼座都需要自定义。因此我采用了将需要换乘的请求分成不同阶段的方法来实现换乘功能。我在Schedule类中加入了一些方法来在原始请求输入时就进行运输路线的规划,并将规划好的路线存储在ProcessingTable中,然后每次取ProcessingTable中的第一个步骤投入相应的电梯候乘表中。在处理某一请求结束时,则在ProcessingTable中查找是否有和当前已结束请求的乘客ID相同的请求,若没有,则说明该请求已处理完成;若存在这样的请求,则从ProcessingTable中取出相应请求,再次投入调度线程的等候表中,进行接下来的调度。由于在输入线程结束后,等候表依然会接受到新的请求,因此我在RequestQueue类中新增了一个变量isInputEnd,用来标记输入线程的结束,使原来的isEnd变量用来表示所有分解后请求的处理结束。为了实现新增电梯的各种参数的自定义,我在Elevator类中也增加了相应的变量。

4、UML协作图

 

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

在这三次的作业中我使用了关键字synchronized,对共享对象进行上锁,比如等候表和第七次作业中的ProcessingTable,因为会有多个线程对这些对象进行访问并进行读或写,所以需要加锁来保证线程安全。同时为了避免输出不安全的问题,我在Printer类中也加上了锁。在Process类中涉及到访问共享对象的位置,我也加上了锁,来进一步保证线程安全,但是感觉有些地方的的设计不够简洁,导致有些同步块略显冗余。

三、调度器设计

第五次作业中,我的调度器仿照第实验代码,将所有电梯放入一个电梯列表中,通过循环将每一条请求按照楼座分给不同电梯的候乘表中,直到读到结束请求,这时,调度线程会为每一部电梯设置结束标志,然后调度线程结束。

第六次作业中,新增了电梯增加请求与横向电梯运送请求,因此我在第五次作业的基础上将电梯列表分成了两个列表,分别是横向电梯列表与纵向电梯列表,以减少查找对应电梯时的循环次数。同时通过条件判断对不同类型的请求采取不同的处理方法。为了实现同一楼层或同一楼座电梯间请求的平均分配,我在Elevator类中加入了historyRecord变量,每当相同楼座的纵向电梯或相同楼层的横向电梯加入电梯列表时,就将所有相同楼座的纵向电梯或相同楼层的横向电梯的historyRecord置为0,而在其他时候,每当有新的乘客请求到达时,就给所有可以处理该请求的电梯按照historyRecord排序,选historyRecord最小的电梯处理该请求,然后该电梯的historyRecord加一。我认为这样做虽然比较繁琐,但是是一种较为简单直观的进行请求平均分配的方式。

第七次作业中,我在前两次作业的基础上增加了请求拆分的功能,并且在Schedule类中新增了一个容器ProcessingTable,用来存储那些分解后但还没来得及投入waitQueue中的请求队列。而只有在Process类中的请求处理结束后才会重新在ProcessingTable中查找相同ID的请求队列,若存在,则取出该队列中的第一个请求放入waitQueue进行下一步的调度;若不存在,则说明该请求已处理完毕。

四、bug分析

第五次作业我的bug主要有两个,第一个是在分析时不够严谨造成的,就是电梯的当前楼层不一定就是电梯当前主请求的起始楼层,因此在某些情况下电梯会出现问题。第二个问题时RTLE,在分析过后发现由于我使用了ALS算法,但是在电梯为空的情况下并未实现捎带功能,因此造成了运行时间超时,我通过增加一些判断与处理对应情况的方法解决了这个问题。由于这次作业比较简单,没有碰到线程不安全的问题。

第六次作业在完成时还是比较顺利的,遇到的bug是由于输出没有加锁而造成的输出时间戳不严格递增的问题,于是我通过新建Printer类对输出进行封装解决了这个问题。

第七次作业我遇到的问题是由于采取了对原始请求进行拆分再选择合适的时机放入waitQueue的做法,但原先的设计是当输入线程结束,waitQueue就会设置输入结束的标志,当没有新请求时不再等待,因此出现了轮询的情况。于是我新增了一个变量isInputEnd,用来标记输入线程的结束,用原来的isEnd表示所有分解后请求的处理结束,来避免出现轮询的情况。但是因为这个方法在判断输入是否结束和标记线程结束时忽略了原子性,使得会出现某几个测试点中调度线程无法结束而导致的超时的情况,因此应用实验代码中信号量的方法来判定请求是否完成。

五、Hack策略

感觉本单元的bug很难找,包括找自己的bug和发现其他人的bug,也碰到过跑一天也复现不了一次的bug。因此这个月基本上就是读代码找bug了,感觉仔细理解一下代码的逻辑也是可以发现bug的,但是效率非常低。

六、心得体会

在本单元我第一次接触了多线程编程,虽然过程中有很多摸不着头脑以及焦虑的时候,但是其实能够感觉到自己对于多线程的了解是不断进步的。而且感觉自己在这一个月收获最大的是凭空debug的能力——写第一次作业debug的时候我并没有搞懂投喂包应该怎么用而一直没有自己生成jar包,是一直看着官方包中code.jar的输出结果debug,却找到了几乎所有的bug。虽然在发现这件事之后有一种被雷劈了似的荒谬感,但是我也觉得虽然这一单元好像没有特别好的debug方法,但是有一些bug可能是非常浅显的,其实只要自己认真梳理一下代码的逻辑就可以发现。所以提前设计完备的逻辑、认真读代码还是非常有必要的。

 
posted @ 2022-05-01 19:06  李沛儒  阅读(16)  评论(0编辑  收藏  举报