2020BUAAOO_Unit2_Summary

一、程序整体架构

  本单元的作业要求我们利用JAVA多线程程序设计方法,完成对电梯运行与调度的模拟。为了能更好的保证程序的可扩展性,我在第一次的任务中就进行了整体架构的思考,希望可以更好的适应后续任务的第二代方向。

  第一个任务中,我们需要完成对一部可捎带电梯的运行与调度的模拟,以总运行时间为性能评价标准。分析要求可以发现,这个过程中存在一个产生请求的生产者,以及一个消耗请求的消费者,可以很好的满足多线程设计中的生产者-消费者模型。基于这一模型,我进行了进一步的分析。我将这一个任务拆分为两个线程,首先,第一个线程实现对请求输入的模拟,并将请求保存到控制器中的托盘中。第二个线程实现对电梯的模拟,完成基本的上下楼,开关门,进出人操作。即请求生产者(生产者)-控制器(托盘)-电梯(消费者)。但是,结合后续的第二代方向来看,这个设计的理念并不是十分完善,存在着一些问题。主要在于我并没有将控制器单独分离成一个单独的线程,控制器既起到了控制的作用,同时也包含着托盘的作用,干的事有点多了。其他的话我跟人觉得还是可以的,没有什么方向上的大问题。

  在第二个任务中,我们需要完成多部电梯的调度,同样要求总运行时间尽可能的短。由于电梯数目的增加,如何给这些电梯分配任务变得十分重要,也正因如此,我不可避免的要单独开启一个线程,我称之为分配者,完成将输入的请求向多部电梯分配的工作。因此在第一个任务的基础上,抛开电梯本身属性的增加,我在结构上加入了分配者,因为整体结构变为请求生产者-请求总托盘-分配者-请求电梯托盘-电梯,这个结构我也一直沿用到了第三个任务之中,个人认为还是不错的。其中分配者的职责在于将总托盘中的认为合理的向不同的电梯分配,将请求输入到电梯托盘中。而电梯本身具有一定的智慧,即电梯知道如何处理已经分配给自己的请求,进而做出行动。

  在第三个任务中,我们的电梯不再是相同的,他们有个性,他们运行速度不同也就算了,关键是他们可以停靠的楼层居然进行了诡异的设定。在此我并不是想批判这个停靠楼层的设定,只是觉得它确实很成功的提升了这一次任务的难度。不过静下心来,我们会发现,只要能活用图相关的知识,进行了最短路径的规划也并不是什么难事,只要你能一点点去弄。因此,基于第二次任务的结构,我对来自请求生产者中的请求进行了合理的拆分,使之成为多个可以被一种类型电梯处理的请求的集合,我们只需要依次派电梯去相应这些请求就可以了。本以为这样就大功告成了,我突然发现,这次的效率评价标准有了微妙的变化。这次的效率不仅要考察电梯的总运行时间,每个请求从提出到完成的时间也同样不能忽视,这对于我之前的调度算法着实是一个不小的冲击,毕竟我之前两次的调度算法都是有可能吧一个人从头管到尾的。针对这种不同的评价标准,我也适当的对调度算法进行了调整,将在后续的部分进行介绍。

二、程序结构分析

  1、Task1

  首先是类图:

  其次是UML时序图:

 

  最后是代码分析:

  结合代码分析结果可以看出,在这次的任务中,控制器承担了较多的工作,不仅要完成托盘的工作,存放请求,同时还要对电梯进行调度。可以考虑将托盘功能与控制功能分离,这样可以减低代码的复杂度。

  2、Task2

  首先是类图:

  其次是UML图:

 

  最后是代码分析:

  根据代码分析可以看出,这次的图复杂度并不是很高,主要还是因为电梯托盘与电梯以及分配者的频繁交互,导致了较高的代码复杂度。

  3、Task3

  首先是类图:

  其次是UML图:

 

  最后是代码分析:

  分析这次任务的代码,我们发现图复杂度有所提高,同时代码复杂度也相比于之前的有所提高。我觉得这一次主要的复杂度集中在电梯请求托盘以及电梯这两个类上,而之所以出现高复杂度可能是因为电梯在处理完一个请求后,有可能需要将请求重新写回到总请求托盘中,同时需要与电梯托盘进行交互以判断如何调度。这一块我觉得如果拆分的话可能会有点复杂,需要对整体结构做一个较大的调整。

  4、SOLID原则分析

  (1)S——单一职责

    在这三次任务中,单一职责原则我们尽可能的去满足。我通过不同的类对功能进行划分,尽可能的避免类与类之间的功能重叠,同时建立了一个专门实现计算方法的静态类,以此来满足在不同类中的存在的相同的计算需求。但是依然存在不足之处,最明显的是电梯托盘这个类实现的并不够理想。在这个托盘中,不仅承担了存储请求的功能,同时承担了调度的工作,这个没有很好的体现单一职责原则。

  (2)O——开闭原则

    在这次的程序设计中,我并没有使用继承的方法,一方面可能是因为我程序写的并不是很好,没法很好的在前一个版本的基础之上很好的添加新的功能,但是另一方面,我也认为为了实现较高的效率,部分重构的方法会有更好的效果。

  (3)L——李氏替换

    由于程序中没有使用继承的方法,同时也没有必要使用,因此不存在对里氏替换原则的分析

  (4)I——接口隔离

    由于程序中只涉及到Runnable类的实现,因此不存在接口隔离原则的分析

  (5)D——依赖倒置

    我认为我的这个程序设计中涉及上下层关系的地方较少,故不存在对一来倒置原则的分析。

三、调度算法分析

  说在最开头,我觉得这次的三个任务对我的调度算法并不算十分友好,因为前两次我们只考虑了总运行时间,忽略了每个请求的执行时间,因此如果想了解针对总运行时间的调度算法,可以重点关注前两个任务;如果想了解考虑到了每个请求时间的调度算法,可以直接关注第三个任务。

  1、Task1

  在第一个任务中,我们的电梯已经初具智慧,并不是傻瓜开局,但是难度也并不算高。

  首先我们分析一下这次的题目要求。一部电梯,所以我们只需要让这部电梯知道自己在某一时刻应该上行,下行还是不动就可以了,至于开关门这种事只要判断一下这一层有没有人要下,有没有人要上就可以决定了。无限容量,这意味着我们不需要考虑满载的情况,只要当前楼层有人,就把人装上电梯,这样我们总是赚的,因为无论如何你最后肯定也要过来接他,只是说要考虑会不会存在折返的问题。

  分析完了题目要求,我们可以开始想想调度策略了。我采用的是SSTF算法,即优先处理离当前位置最近的请求楼层,这个请求既要考虑电梯里面已有的请求的目的地,也要考虑电梯外请求的出发地。通过这个算法,在不考虑预知未来的情况下,可以达到较优的调度。因为不论是电梯里的人,还是电梯外的人,我们最后肯定要去解决他们,即前往他们的楼层,那么如果你选择离当前位置最近的一个请求,你所需要的折返是最小的。这个我并没有什么严谨的证明,有一定的唯心主义成分,不过经过了几次的实验,我发现就针对一静态的请求分布,这种算法的结果总是最优的。

  在认可了SSTF算法之后,我们将它实现就不是什么难事了,而且在后续的任务中,我也会经常使用到这个算法。

  总结一下,或许SSTF在这一条件下并不是最优解,但是它已经是比较优秀的调度方法。之所以强调这一点是因为,在这个单元刚开始时,我致力于找到理论上最优的调度方法,以此来取得完美的性能分。但是抛开预测未来不说,随着任务的推进,调度的过程越发的复杂,以至于难以找到一个理论上最优的算法。如果你一心想要找到最优的算法,可能会在这上消耗大量的时间,而且最后可能也没弄出什么有用的东西,还因为没时间检查而漏洞百出,这是我们最不愿意看到的。因此我觉得我们的算法不需要是最优的,只要它不是最差的,我们都是可以接受的,在此基础上,我们可以做进一步的优化。

  在这次任务中,性能得分还算看得过去,总分97.5分。

  2、Task2

  在这个任务中,我们需要操作的电梯变为了多部电梯,同时每部电梯也出现了人员的限制,这两个是我们这次最大的挑战。

  一开始我考虑这个任务时,陷入了寻找最优解的怪圈,一直因为自己的算法不是最优而迟迟没有动手,消耗了大量的时间。

  /*期间也有一个极端的想法,不妨在这里当个笑话分享一下。大部分人的调度应该是这样的,当有一个请求发出后,通过一套评估逻辑或者是方法,选一个最优的电梯,把这个请求分配给他,从此撒手不管,即不会在将这个请求分配给其他电梯。但是这样肯定不是最优的,更好的处理方式应该是结合所有的等待电梯的(在电梯外的)请求,综合考虑,进而评估出一个最优的分配方法,这个过程需要在每一个请求加入的时候执行一次。可是,我比较愚笨,没有想出一个可行的分配方法,但是我想到了一个枚举的方法,即通过将所有电梯外的请求通过排列组合的方式,分配给不同的电梯,进而计算运行时间。然而,我们做一个简单的计算,根据指导书,我们最多有50个请求,5部电梯,那么如果50个请求同时发出,我们的分配者就要对50个请求进行重新分配,根据排列组合知识我们知道,这样会产生5^50个分配方案,这个数字好像有点大。当时我试图遍历这每一种分配方案,以此来寻找最优的分配方法,结果可想而知(其实我并没有写成代码)。*/

  说了这么多废话,让我来说一下我最终选择的分配方案。  正如大部分人一样,我也是在一个请求到来的时候就进行分配,同时这个分配就是永久的,别的电梯不回去做没有分配给它的任务。如何评价分配给某一部电梯这个决定是好是坏呢,我进行了电梯运行时间模拟计算,即根据SSTF调度算法,考虑电梯的运行时间以及开关门时间,计算当这个新请求分配给这个电梯之后,需要运行多长时间这部电梯才可以空闲,这里考虑的请求当然是既包含电梯内也包含电梯外已经分配给它的请求。通过这种电梯运行时间的模拟,我们可以选择运行时间最短的电梯,将新加入的请求分配给它,从而实现在这一时刻整体的时间最短。

  再说一说人员的限制。这次的电梯不像第一次任务中的无限电梯,电梯中的总人数受到了限制,因此在进行模拟时间计算的时候,我们同样应该考虑到满员的情况。如果发现电梯中已经满员,那么我们将不考虑电梯外已经分配给它的请求,在只考虑电梯内请求的条件下进行SSTF调度算法,完成电梯的一次调度,这样就可以很好的处理人员限制问题了。

  而对于每部电梯本身,调度上可以完全参考模拟时间计算中所采用的方法,进而做出上下行的决定。

  3、Task3

  这个任务可以说是在难度上有一个较大的提升,但是依旧在我们的控制范围之内,只要将任务进行拆分,我们会发现他也不过如此。

  先来分析任务要求,相比于第二次任务,我们的电梯到达的楼层变得个性化了起来,至于运行时间以及载客量的细微改变,我们只需要在模拟运行时间计算中进行细微的改变就可以应对。

  那么针对可到达楼层的个性化,我们有什么好办法呢。我注意到有些人用了一些奇怪的表格,说实话我并没有仔细的看,因为我觉得单纯的利用图结构中的最短路径规划就可以很好的解决这次的问题。相信大家都学过了数据结构,对图的最短路径规划问题肯定不会陌生,说白了我们用个深搜也可以很好的解决这一问题。那么我们如何在这次的任务中定义最短呢。我的判断标准是在到达目的地的前提下,优先最少换乘,其次是运行时间。由于多次的换成涉及到对其他电梯的调度,麻烦别人总是不好的,毕竟别人也不是闲着,他们往往也有事要做,同时开关门的时间花销也是不能忽略的。根据以上的判断标准,我实现了最短路径的计算,进而将输入的原始请求分割为多个由一部电梯就可以执行的请求序列,将这个序列作为一个整体,投入到总请求托盘中,之后就可以向正常请求一样进行分配了。

  对于一部分同学来讲,可能到这里就已经解决的所有的难点,电梯的调度这一块只需要沿用第二次任务中的方法即可。然而,我突然发现,这次的性能评价标准发生了亿点点的改变,那就是对每个请求执行时间的限制。针对新的评价标准,每个请求的处理时间必须要进行考虑,因此单纯的使用SSTF算法,很有可能会导致一些远程的乘客因为一些短程的请求而被困电梯,出现饥饿的现象。为了避免这种现象的发生,我对我的算法进行了调整,一句话来说,我的电梯不会轻易的变换方向,除非当前方向上已经没有新的请求,即SCAN算法。采取这样的算法可以保证进了电梯的人不会被电梯反复横跳而无法到达,最不济也能在一个来回中到自己的目的地。显然,对于很多情况,这样的调度算法与SSTF算法没有可比性,会造成总时间的增加。然而,这种算法一方面是比较贴近现实,另一方面这个方法可以更好的保证每个请求的执行时间,我们最好还是不要去赌不存在反复横跳的情况,这样的投机心理往往会导致大问题。

  这次的性能还是挺不错的,总分也是拿到了99.7,可见这种算法也还是可以用的呢。

  4、总结

  总体看下来,不论是那一次作业,我的算法显然不是最优的,可能会有很多可以改进的地方。但是,我并不认为我们需要一味的追求最优,在实现难度与效率这两方面应该做出权衡,为了100-99.7的效率分,我们没有必要再多花费一倍甚至两倍的时间(而且也不一定有提高)。同时,在效率上消耗大量时间,势必会影响你花费在Debug上的时间,最坏的情况可能会导致出现WA,这样就得不偿失了。所以,我在效率与实现难度上做出了权衡,没有过分追求效率,保证了足够的测试时间,进而有了以上的调度方法,并且以结果论来看,还算可以接受。

四、BUG总结与反思

   在第二单元的任务中,我积极地使用了自动化的测试程序,很好的避免了BUG的发生,在第一次以及第三次的强测中均未被发现BUG,但是在第二次强测的过程中,出现了有关输入缓存区的BUG,在此进行一个分析。

  在第二次任务中,我们需要在程序刚开始时输入电梯总数,为实现这一目的,猪脚给我们提供的输入官方包中存在着一个专用的方法,然而我一开始并没有注意到官方包中提供的电梯数目输入函数,因而使用了通常使用的Scanner类。本以为并没有什么不同,而且虽然注意到了 讨论区中有同学提出关于输入缓冲区的问题,但是我并没有重视起来,只是进行了相对简单的测试就跳过了。最终我使用了在main函数中创建了一个Scanner对象,使用nextInt方法进行了电梯数目的读取,完成读取后,我又创建了请求生产者对象,在这个对象中实例化了官方包中的输入对象,进行后续的输入操作。

  这样下来虽然没有像有些人那样出现大规模的吃人现象,但是依旧有两个点惨遭毒手。分析原因,我认为是因为在读取到了电梯总数输入后,main函数中的输入缓冲区没有及时关闭,而这时第一条请求已经到达,它进入的是main函数的缓冲区,没有正确的进入到官方包所在的请求生产者类中的缓冲区,进而造成这个请求无法被输入,也就发生了第一个请求无法相应的情况。

  总的来说,这次之所以出现这个BUG其主要原因还是因为没有使用官方包提供的输入方法,如果注意到了这个输入方法的存在,我应该不会在使用Scanner类,进而就可以很好的避免这个BUG。

  多说一点,在第三次任务中,我的程序其实是存在问题的,然而强测并没有发现(赚到了)。当时提交的时间已经截止,我依然用我自己写的测试程序进行评测。本来以为万无一失的时候,突然有一个数据点给我报出了一个HashMap遍历过程中对HashMap进行了修改的错误,我顿时就慌了。仔细分析后发现了一个线程不安全的重大隐患。由于第三次作业涉及到电梯的增加,我便建立了一个HashMap用于存储所有的电梯对象,一个HashMap用于存储所有的电梯托盘对象。每当请求生产者线程得到一个增加电梯的请求后,我就会向这两个HashMap中添加一个新元素。在后续的调度算法实现的过程,我也会便利这两个HashMap以寻找最优的电梯选择。或许到这里你会觉得并没有什么事,但是仔细一想我们会发现,由于电梯生产者后后续的调度属于两个不同的线程,而且这两个HashMap也并没有进行任何线程相关的保护,这就导致了如果在调度遍历HashMap的过程中增加了一个元素,这就会导致之前我提到的错误。而且由于我们需要对两个HashMap进行添加元素的操作,这两个操作无法同时进行,存在被其他进程打断的风险,而在调度的过程中,我根据电梯HashMap的key不仅找到了对应的电梯,同时也会使用key在电梯托盘的HashMap中寻找对应的托盘,可这两个可说不定是不是同时存在的,这就会导致访问到空key的情况发生,进而报错。为了应对这一问题,一方面我们要使用线程安全的容器,另一方面我们只要适当的修改添加过程与遍历的的顺序,即遍历后添加的HashMap,这样就可以保证只要在后添加的HashMap中遍历到了key,那么先添加的HashMap中也一定能找到对应的元素,从而避免BUG的发生。

五、自动化评测

   吸取了第一单元的教训,这一次我设计了自动化评测的程序,可以分为四大部分,分别是请求生成器,输入器,检查器以及连接脚本。

  1、请求生成器

  相比于其他几部分,这一块其实没什么可说的,说白了就是按照指导书的要求,生成若干条请求。我要注意的是首先请求的数量一定不能超过指导书中要求的上限,这个是硬性要求。其次,我们要用自己写的程序跑一跑这些请求,看看自己执行多久才能搞定,毕竟指导书中有规定,你的运行时间不能超过70s(恼火)。最后,我这次指令的生成是完全随机的,并没有什么策略可言,即不会集中在一个时间点生成,不会集中生成某种极端数据。不过即便如此,依旧可以在互测中吃香喝辣,所向无敌(第一单元进了一次C屋,身上已经沾满了鲜血,甚至忘记了给别人留一点面子)。

  2、输入器

  这一部分的功能就是按照请求序列给出的时间,按照时间将请求文件中的所有请求多输出到标准输出中就可以了,其实也没什么技术含量。如果有人问为什么要输出到标准输出中,且听我后续为您道来。不过要说麻烦的地方,主要还是一些文件操作不太熟悉,消耗了一点时间。

  3、检查器

  这一部分应该是最为复杂的一部分了,他担任着对输出进行检查的工作。为了能对输出进行完整的检查,它需要指导请求文件中的请求,他需要知道输出文件中的输出,也还要将所悟的信息输出到统一的错问文件中,听起来就挺复杂的,而且这还只是文件操作。更为核心的是如何判定某个输出是错误的,我这里采用的是模拟电梯运行的方法。当然,我不是说要写个多线程的东西,而是创建多个电梯类,在电梯类中记录着当前电梯中的人,楼层以及开关没之类的状态,根据输出文件中的信息,我们将判断电梯是否可以进行这个操作,如果可以,那就过,如果不行,那就报错。最后,我们还要总体控制一下,检查一下电梯都关没关门呀,人都到没到呀,诸如此类,如果一切安好,那你就AC了(至少我觉得你AC了)。

  这里在提一个小建议,可能你看我上述的描述觉得好像并没有很复杂,但是当你真正做这个检查器的时候你会发现,因为可能出错的地方真的有很多,你的程序会被大量的输出所占据,变得杂乱不堪。我觉得是否可以选择函数的方式,或者抛出异常的方式,这样可以将不同的错误归为函数,如果发生这种错误,我们就调用某一个函数,更进一步我们可以根据这个错误的类型,类似工厂模式,进一步作出不同的处理,也有点像操作系统中的异常处理方式,这样的话可能会简介一些。

  4、连接脚本

  这一块虽然只是起到将上述三大模块连接的作用,但是我一开始最不知所措的就是这里了,因为我根本不会按照时间的向一个java程序中输入请求呀。经过了一系列的研究我发现,python、java都可以实现类似的线程管理,从而做到在一个程序中调用另一个程序作为线程,并向这个线程中输入一些东西。不过,有一说一,我没看懂,所以这部分到此结束(逃)。

  因为没看懂python或者java的实现,另一方面也是担心这些线程操作中出现问题,我选择了传统也相对简单的bat脚本,利用了其中的管道功能,成功的实现了将输入器的输出作为待测程序的输入,由于输入器是按照时间向标准输出输出请求,因此时间的控制也很好的实现了。至于其他的bat脚本实现我这里就不细说了,最重要的就是管道功能,我认为它简单而且有效(就是不知道这样会不会测试效率低下?)。

六、总结

  整体来看这次的作业完成的还算不错,得到的成绩也还算比较理想,同时实现了自动化的测试,在互测过程中也成功的和同学们进行了儿有好的交流,受益颇多。不过要说不足的话还是存在的,最主要的不足可以说是在迭代开发上,每个新的任务我并没有使用继承的方法进行迭代,而是将其中部分类或者方法进行重构。虽然不是整体架构的改变,但是也是费了不少功夫,这一块的话我其实也不是很明白,因为你想,为了能很好的适应每次的作业,我肯定是向把那些没用的东西去掉,只留下这次要用到的东西,没必要写的就不写,调度算法上也是一个道理,为了适合不同的评价标准,整个算法肯定是全盘的改变,难以实现迭代。其次还有个小问题,在这次的测试中,我做的测试只是黑盒测试,并没有使用其他的测试方法,这一块我还需要进一步的学习,争取在下一单元中实现更多的测试方法,保证程序的正确性。

猜你喜欢

转载自www.cnblogs.com/zyy-student/p/12717203.html