2021OO第二单元总结

2021OO第二单元总结

19231142 李靖尧

一、第一次作业:

  本次作业的要求是设计单部电梯,分三种人流状态实现不同的运行模式。电梯的运行方式也比较简单,只能上下行,在指定的楼层将乘客送达。由于这次作业是我第一次接触多线程,在设计过程中遇到了不少意想不到的问题,整体代码架构的构思也花费了不少时间。个人认为,第一次作业是多线程三次作业中最难、最耗时的一次作业,毕竟从0到1远比从1到100难多了。

  在代码架构方面,具体类图如下:

 

  可见,一共开了两个线程:电梯(Elevator)和输入端(InputHandler)。其中为电梯配备了一个控制器Controller,获取电梯状态,判断其是否需要开门、运行方向等。两个线程共享一临界资源——全局的请求池(RequestPool),其本质就是一个存放请求的数组。输入端负责获取用户请求,并将其放入RequestPool中,而电梯发现RequestPool中有请求,就将其取走(至于取法,下文关于算法的讲解会提)。这就是典型的生产者(输入端)—消费者(电梯)模式。看似寻常的设计,却在具体实现时暴露出严重的问题:程序什么时候才能结束呢?从理论上讲,生产者不再生产,消费者不用再等的时候程序就可以结束了。对应到代码的实现,程序结束的充分必要条件是:

If all clients served and no more clients:

Program exit

  于是,在Controller里判断电梯结束运营的逻辑是这么写的:

 

  可见,外层while的逻辑是在没有请求的时候让电梯线程进行等待,如果接到请求或者输入结束,就被唤醒,跳出while。因此,还需判断到底是哪种情况,如果是输入结束,那么需要让电梯结束。逻辑精妙世无双,然而一运行,就出问题。究其原因,就是多线程并发的常见问题。简而言之:当请求池已经空了,电梯在等待新的请求,这时输入结束了,唤醒电梯,然而while的条件仍为真,电梯还会等,以后就再也唤不醒了,出现“死锁”。其次,就算能够跳出while,现在需要判断。如果输入结束信号到达时,请求池中还有未处理完的请求,那么程序会直接结束了,出现“早退”。究其本质,就是这两个判断遗漏了情况,导致出现逻辑漏洞。

  于是,更改如下:

 

  这样,就顺利通过测试啦!

  在电梯运行逻辑和接人的算法上,我花费了大量时间研究了Look算法,并将其应用,具体算法如下:

 

  这样的好处就是可以在保证每个乘客到达的基础上,通过增加电梯上下一趟的效率,减小电梯运行总时间。从强测结果来看,这样的算法在random模式下,还是比较吃香的。

 

二、第二次作业:

  第二次作业在第一次的基础上,增加了电梯的数量,扩展了增加电梯的指令。在代码实现上,最明显的变化就是为每个电梯都开一个线程,有几个电梯,就开几个线程,同时,每个电梯和输入端之间共享同一个请求池。用生产者-消费者模型来看,相当于增加了消费者的数量,这样消费者之间可以共同竞争同一个请求。说到了“竞争”,不得不说一下这次作业的调度逻辑。

  从我的调查结果来看,大概分为两类——“分派型”和“竞争型”。分派就是指构造一个全局分派器Dispatcher,输入一个请求后,直接通过特定的算法分配给某个电梯。这样,电梯就可以不用和请求池直接联系了,它的沟通对象就是分派器,分派器给电梯派什么人,它就去接那个人。这样做的好处是简化了电梯的逻辑复杂度,只需要实现简单的上下行、开关门、上下人即可,还有就是与Dispatcher的通讯。同时,这种做法也比较符合“分而治之”的思想,电梯只需要管好舱内的人,并不关心其他电梯或者正在等待的人。而Dispatcher起到桥梁的作用,告诉电梯接哪些人。另一种是竞争型,不设计专门的分派器,而是所有电梯自由竞争,一有请求,大家蜂拥而上,至于这个人最后谁接到了,谁也说不好。这种做法也有好处,一个是简化了分派的逻辑,取而代之的是“抢人”的逻辑,写起来较为简单。问题就是,会很容易出现所有电梯一哄而上,却只有一个抢到了人的情况。这样其实降低了其他电梯的运行效率,提高了整体系统运转的积极性。我最后选择的是“竞争”的算法,强测表现还不错,获得了99+的成绩。

  由于第一次的良好基础,这次作业写起来并不是很费力。在互测中没有被人找到bug。具体类图如下:可以看出,和第一次没有很大的出入,仅是将三种模式的算法抽象出三个类,这样逻辑较为清晰。

 

三、第三次作业:

  第三次作业最主要的变化就是增加了电梯的种类,一共分为三种,A为每层可停靠型,运行最慢,但载客数最多。B为奇数停靠层,运行速度中等,载客数量中等。C为高低层停靠型,运行最快,但载客数量最少。并且这次作业增加了换乘机制,乘客可以中途在某层下电梯,再坐电梯到达目的层。

  为了较快将乘客送达,需要尽量乘坐较快的电梯,这样在某些情况下需要考虑换乘。例如从1层到17层的请求可以先乘坐C电梯从1层到18层,然后换乘A电梯从18层坐到17层。然而需要注意的是,本次作业的性能分有了较大的调整,每个乘客的等待时间都化为了性能评判的一部分。这意味着,电梯为了接一个换乘的人而需要开一门,就会耽误了电梯内所有人一次开门的时间。有点像一个40人班老师常说的一句话“你一个人耽误1分钟,就等于耽误全班同学40分钟时间。”因此,疯狂换乘,就会导致疯狂开门,这显然是不合适的。由此看来,这之间存在一个“度”,从某层到某层可以换乘,从另一层到某层可能换乘就不太值了。综合考虑换乘和开门的开销,我总结了一张换乘表如下,可以粗浅理解为5层和15层是换乘点:

 

  那么,具体到代码的实现,怎么才能做到换乘呢?可以这样想,输入端输入请求后,被电梯获得,载他走一段路程后,到把他放下,具体说就是给请求池输入一条这个人剩余路程所对应的请求。看着好像逻辑很复杂,需要读取请求,计算在哪换乘,判断需要哪个电梯接这个人,在哪放人,哪个电梯在接……因此,可以考虑单独抽象出一个类,专门负责换乘信息的计算,比如命名为Dispatcher。从数据结构角度来讲,感觉输入端输入的请求和电梯换乘抛出的请求似有相似之处,毕竟本质上都是PersonRequest么,但是二者产生位置和时间不甚相同,这怎么处理呢?可以考虑将其封装起来,成为Person类,意义与先前有所不同,这是一个“人”,而不是一条请求,“人”比“请求”多了换乘的信息,电梯看到这个人,就知道他从哪上,到哪换乘了。

  Person类内部结构如下:

 

  可以发现,“人”是输入端输入的“请求”拆分后再组合的结果,设置数组存放拆分后的请求,满足前一个请求终点是下一个的起点。其中内部变量index指的是这个“人”目前被处理的请求是拆分后请求的第几个,同时对外提供方法currentRequest让外界可知当前这个人的请求。与之配套的是update方法,每次该换乘,电梯将这个人踢出来后,调用update方法,执行index++操作,表示当前请求完成,指向下一条请求,如果发现指向数组末尾了,就意味着这个人到达目的地了,不用再放回请求池了。那么这样,电梯抛出的请求和输入进入的请求就合二为一了,同为Person,符合面向对象的“封装”思想。

  那么,怎么进行输入指令的拆分呢?这里的逻辑全部封装在Dispatcher中。简言之,它的功能有:拆分输入请求,包装成一个人,加入到请求池、电梯换乘剔除某个人后,调用update方法,在根据情况放回请求池,检测输入是否为空。运行流程如下(前后Dispatcher为同一个):

 

  三种模式下的UML顺序图如下:

  1、Morning模式:

  2、Random模式:

  3、Night模式:

 

  关于互测 :这三次作业在互测中既没有找到别人的bug,也没有被找到bug。

四、总结:

  通过这几次的作业,让我对多线程有了初步的认识,了解了并发当中一些问题,在寻求解决方法的过程中,锻炼了我的编程能力。同时,编程模式的训练让我对代码架构有了更清晰的掌握,正如课程组所言:一个好的程序的基础是拥有一个好的代码架构。

posted @ 2021-04-25 11:03  春天里666  阅读(101)  评论(1编辑  收藏  举报