OO第二单元总结(电梯)

一.综述

  第二元的主题为电梯,即构建一个或多个电梯,当标准输入中有请求时,使用电梯将他们送到正确的位置,电梯的每一项操作都要对应在标准输出中显示。第一次只有一部电梯且不限制载客量,第二次有多部电梯且限制载客量,第三次有多部电梯,可以中途加入电梯,且载客量与可到达楼层都不尽相同。性能分取决于程序运行时间,第三次还加入了乘客等待时间作为性能标准。

  此次作业明确要求使用多线程。

二.作业与BUG分析

第一次作业

1.代码思路

  第一次作业总体思路是分两个线程,输入线程与调度线程(原本输出占有单独线程后发现百害而无一利,遂放弃),两个最主要的类分别是电梯类与电梯调度线程,前者用于储存与处理电梯相关的各项信息,同时执行上下人等操作,后者用于调度电梯,通过各种算法实现电梯的最优运行。同时为了更好地储存请求信息,还建立了Floor类与Person类等,建立Storage类,内含Floor数组,用于储存请求信息。

  此外,利用0楼请求作为结束标志,这使得Floor数组中楼层刚好与下标对应。

  电梯算法模拟现实中的电梯,不过更加制杖。电梯从1楼向上走,只要某一层楼有人发出请求就装进电梯(即便电梯向上请求向下也装入电梯,这样性能反而更好),有人到达就离开电梯,直到更高楼层没有进入电梯或离开电梯的的请求,电梯便变更模式开始向下走,以此类推,没人时停在中间楼层。

2.多线程设计策略

  第一次作业我认为难度最大,耗时最长,因为需要从0开始进行多线程的设计,采用了生产者-消费者模型后,将输入线程作为生产者,调度线程作为消费者。电梯类作为调度线程的一个成员变量,本身不是线程,但是具有楼层,装载人员,最大载客量等一系列属性与开门、关门等方法,调度线程本身不再构建这些功能,只需要在合适的时间调用电梯类中的方法,这样就极大地减轻了调度线程的压力。而当调度线程中没有待调度的指令时(即电梯空闲),调度线程就会调用pause方法,此方法会返回请求队列的信息,但是在请求队列为空(即当前为空闲状态)时wait,等待新的指令进入将其唤醒。

3.代码度量分析

UML类图

  第一次作业我就大体将整个系统构建了起来,不同于以往一main到底的形式,这次我的主类得到的极大地缩减,以至于main函数中只有寥寥十几行代码。这使得其功能更加明确:作为总指挥,负责指令池、请求队列的初始化,线程的启动,两个输入线程与debug模式的选择,还有将楼层等关键信息传入其他部分中。除此之外几乎没有任何冗余的执行与运算语句,主要工作几乎完全交给了其他线程与类,主类空旷的我都感觉有点矫枉过正。而两个核心类——电梯与电梯调度线程——内容则极为丰富,起到了一部分去中心化的作用,使得整体程序结构更加平衡。

复杂度分析

  

  可以看出,复杂度最高的是电梯调度线程,其中的run方法由于涉及大量操作与判断,因此复杂度极高ev,iv和v分别达到了9,13,14,除此之外needup与needdown两个判断线程也有较高的复杂度。

4.bug分析

  此次作业在强测和互测阶段均未发现bug。

第二次作业

1.代码思路

  第二次作业在第一次作业的基础上,在电梯线程与输入线程中间新建电梯调度线程,同样采用生产者-消费者模式,即输入-电梯调度-电梯i的模式,电梯调度线程负责将输入的请求根据电梯位置与请求本身进行动态分配,将不同请求发送给不同的电梯。同时在第一次作业中保留的weight属性与上下行请求的分类在此次作业中也派上了用场。

  电梯算法相比上次电梯进行了改进,为了节约载客空间,每部电梯在到达某一层楼时,只有遇到请求方向相同的人才装入电梯。电梯调度线程分配时,将请求优先分配给到达请求时间最短的(即默认电梯在底楼与顶楼之间往复,选择到达请求楼层且方向相同时所需层数最少的电梯),且未满员的电梯。

2.多线程设计策略

  第二次作业在多线程上并没有多大的创新,甚至还使用了几个线程不安全的方法(但是不会造成严重后果)。将第一次作业中的调度线程由总调度改为分调度(即每个电梯都配备一个第一次作业的调度线程)。因此只需要将指令传递给电梯,电梯就会像第一次一样自行调度,接送乘客。
  而在更宏观的电梯调度层面上,我则采用了较为简单的方式,即同样的生产者-消费者模型,每个电梯都有一个指令池,同时总调度线程与输入线程之间有一个请求队列,输入线程将获得的请求传送到队列中,总调度线程每次从中读取一个并将其分配各电梯(即发送给相应的电梯指令池中),此请求便与该电梯绑定,不再乘坐其他电梯,等待该电梯接送。不过,总调度线程获得电梯信息时,我图方便采用的直接读取,因此可能会存在一些线程不安全的情况,但是读取到的信息与当前信息不同步,只会导致分配的电梯可能不合适,并不会造成严重后果

3.代码度量分析

UML类图

  第二次作业增加了控制中心线程,用于进行请求的动态分配,这使得其开始呈现“三足鼎立”的局面,三个主要的大类占据了绝大部分代码量,也承担了绝大多数工作。其余的小类以这三者为中心,承担一部分数据存储与服务等功能。

复杂度分析

  

  可以看出,此次作业中复杂度普遍有所上升,不仅上次复杂度最高的三个方法复杂度进一步上升,同时控制中心类里用于计算电梯到达时间的方法与run方法复杂度也很高。

4.bug分析

  此次作业在强测和互测阶段均未发现bug。

第三次作业

1.代码思路

  第三次作业与第二次作业相比,架构变化不大,主要是由于难度加大而导致复杂度上升,换乘乘客下电梯后向调度线程发送新指令重新分配电梯,这使得调度线程必须在请求为空、收到结束指令且所有电梯都空闲的情况下才能结束,增加了判断标志,同时将新增电梯指令和乘坐电梯请求都使用同样的person类表示,在收到请求时电梯调度线程首先分类,然后进行相应操作。对于换乘部分,则大幅修改了原有的电梯运作和下电梯的判断条件。

  分电梯调度算法大体不变,不过乘客换乘时会优先在当前电梯内乘坐尽可能多的时间,即只有当电梯无法使乘客与其目的楼层的距离更近时,乘客才会选择换乘电梯。而总调度算法则在分配电梯时考虑了换乘因素,即便某电梯无法到达目的楼层,只要能使乘客与目的楼层之间距离缩短,也可以分配给该电梯,计算最佳电梯时综合考虑了等待电梯时间、电梯运行时间与剩下的楼层数量。

2.多线程设计策略

  第三次由于之前对难度估计不足,没有留出足够的改进空间,因此在架构等方面有一些捉襟见肘。由于涉及电梯换乘,每次换乘乘客从电梯中出来,都要变换为新的请求传送给总调度线程,这就使得其实际上成为多生产者的生产者-消费者模式。其停止就要保证所有的生产者都结束时,因此即便接收到了输入线程发送来的终止信息,也要暂停而非中止,每次当有其他电梯运行结束时将其唤醒使其重新判断结束情况。

  总体上相对前两次,在多线程上基本沿用了前面的布局,电梯分配仍然是只将指令分配到电梯,在该乘客从电梯下来换乘时便将其认为新的指令,总调度线程只用于调度和分配,同时由于此次总调度线程对电梯线程的信息读取直接关系到线程的结束,因此将第二次作业中的线程不安全方法进行了重新编写。

3.代码度量分析

UML类图

  此次作业中的三个大类彻底成了怪物,承担了绝大多数的工作,放在类图里简直鹤立鸡群。

复杂度分析

  

  此次作业的复杂度显著上升,其中复杂度最高的是以下这些:

  由于判断条件变得复杂,因此电梯状态判断的几个相关方法复杂度得到了极大提升,另外分配电梯算法所需的电梯距离计算与最佳电梯分配的算法复杂度也居高不下。

4.bug分析

  此次作业在强测和互测阶段均发现了bug,实际上,在本地调试阶段就出现过许多bug,但是中测与弱测和本地测试并没有将其全部检出。

  此次bug十分严重,且十分弱智,但是我仍然没能及时发现导致酿成惨剧。

  • 由于我采用的电梯调度算法使得3楼相关的请求变得棘手,我不得不付出大量的代码量来专门处理与3楼相关的请求,即便如此,仍然在一个判断条件中输错了变量,导致结果没有全覆盖,在某几种特定情况下会将请求分配给无法到达的电梯使得程序无法正常结束。
  • 由于我的结束判断采用的是电梯空闲、请求队列为空、接收到终止指令,但是电梯在空闲时会自动去往固定楼层停靠,因此如果电梯在往停靠楼层移动过程中接收到指令,由于电梯移动过程中线程正在睡觉因此电梯状态不会马上变化,调度线程就会误认为电梯空闲导致线程结束,而如果该乘客进入电梯后需要换成,因为调度线程已经结束就会在到达换乘楼层后迟迟无法得到分配。

三.第三次作业架构设计的可扩展性

  第三次作业总体上可拓展性还是有的,但是可能相对其他同学的作业可拓展性要小一些。

  • 多线程中,生产者-消费者模式多次使用,贯穿始终,犹如整个程序的骨架,如果对多线程提出更高的要求,可能仅凭该模式无法满足,需要大改。
  • 我在程序中已经利用了一部分非法请求作为指令,承担新增电梯,发送停止请求等任务,使得今后非法请求的利用空间变小,如果非法请求无法满足新增指令,可能需要另外增加数据传递的通路。
  • 电梯调度方面,为了线程安全与在前几次作业中尽量保证性能,多电梯之间的指令池各自独立,因此每一部电梯都执行且仅执行自己指令池内的请求,多电梯间的协同完全依赖总调度线程。这使得为了尽量保证性能,总调度线程的压力每次作业都骤增,到了第三次作业时其压力已经很大,很难继续拓展,如果不能在保证性能的基础上打通各电梯之间的指令池,或者采用更加精简的调度方式。可能再拓展几次,总调度线程的压力讲会变得无法忍受。
  • 由于过于依赖电梯调度、电梯、总调度三个类,在其中加入了大量成员变量,使得其内聚性进一步上升,已经很难将某一部分抽离。而三次作业中越来越僵化的思想使得在今后的拓展中,这三个类可能会变得越来越庞大,实质上仍然相当程度上受之前一main到底思路的影响,尽管过长的方法没有了,但是在一个类中处理大量问题同样不是好习惯。随着进一步拓展,其复杂度可能还会急剧上升。

  综上所述,由于整体思维的僵化,在此架构基础上的可拓展性变得越来越小,第三次作业已经快到极限,如果进一步拓展的要求过高,或许重构才是最好的办法。

四.分析发现他人bug的策略

  此次吸取了之前的教训,在编码阶段就寻找所有可能出错的点并做好记录,然后针对性构建测试数据并将其应用于本地测试和bug互测。同时本地测试一些出错的数据也做了记录。不过由于其他同学的程序普遍较好,因此hack陈功率很低,三次互测每次都投放了近10个测试点,但是一共只发现了别人的两处错误。

五.心得体会

关于迭代

  此次作业由于认识到后面的作业只可能是前面作业的改进,同时又有git保底,因此全面放弃了之前在第一单元中为了保证前面程序的完整性而重复造轮子的憨憨操作(我在当时已经有多项式类的基础上,又从头编写了一个支持括号运算的多项式类,原先的多项式类只用于没有括号时的多项式处理,二者之间通过递归化简。后来想了想,带括号的多项式也是多项式啊!多项式加法什么的都是通用的,我为何要自己给自己找麻烦呢?),每次都在前面作业的基础上进行修正,极大地减小了工作量(或许也与这次作业的连续性比较强有关)。同时由于对后续改进的预测较为准确,因此并未对之前的代码做太大的改动。

  在此单元中,由于三次作业架构相同,整体算法思路也相同,因此并未出现第一单元中第三次作业表达式内突然支持括号与字母而引起整个结构的天塌地陷紫金锤,每次只要对之前的代码略作改进即可扩充其功能,这使得在第一次作业花了较大功夫完成后,后续两次改进在代码量上都较为轻松。在今后的学习中,更应注重代码的拓展,毕竟重构这种事情可不是谁都能顶的住的。

关于代码结构

  此次作业在电梯构建上较为借鉴了计组上将时序部分与非时序部分分离的思想,将非时序部分作为时序部分的一个模块,最大程度上达到高内聚低耦合的效果,实际效果也比较理想。不过有些遗憾的是这电梯、电梯调度、总调度类中的具体功能我当时认为没有必要再细分,因此这几个类越来越臃肿,以至于在后续的作业中成为了怪物般的存在。以后在架构的平衡上还要继续多下功夫。

  另外,多加注释真的是一个好习惯。

关于方法划分

  此次作业抛弃了之前不到万不得已不另开方法,一个函数到底的思路,于是几乎再也没有出现过超过60行的情况。对于一些比较复杂的判断方法,采用单开一个返回值为boolen类型的函数(方法)用于判断,就极大地减少了主函数中的冗余语句,同时debug的时候也便于查找问题。分线程中的run函数就将大量内容都移到了其他方法中,整体只起一个架构的作用。当然,有一点不好就是,方法开的太多,以至于有些方法我找的时候都经常找不到……另外,对于复杂度较高的方法,可以将一部分内容剥离,另开方法,作为方法的方法(禁止套娃),以减少方法的复杂度,降低调试的难度。

关于多线程

  多线程的核心就是多个线程之间的协调运作,甚至可以说,上学期计组的流水线就是一种多线程,每个时序部分都相当于一个线程,不过就是线程间的信息传递方式比较简单罢了。初次接触多线程,也没有多少进阶练习和知识讲解,确实难度不小,尤其是如果没有个明白人跟你说的话,自己学起来还是挺费劲的。但是从逻辑上来说,多线程其实又不难,我们日常生活中的许多事情,其实都可以理解为多线程。将它们写到程序里,就要考虑如何让这个你不跟他说明白就会给你搞事的憨憨知道自己什么时候该做什么,因此就有了各种各样的方法语句。以我个人的角度来说,想出思路还是容易的,比如某个线程负责做什么事情,真正难的是怎么利用家徒四壁的知识,把这些想法付诸实践。

关于性能

  此次程序的性能取决于电梯完成请求的快慢(第三次还加入了乘客等待时长),实际上的发挥空间还是很大的,不同于函数求导中多种多样的化简方式,也就是有多个基本上相互独立的优化方法。这次的性能基本就取决于电梯调度算法的实现,因此影响性能的因素较为单一,不过也有一些能够额外给性能加分的点,比如没人的时候设置电梯等待楼层等。

  电梯的运行算法,我小时候还专门研究过。这次我在论证如何能提高电梯性能时,思来想去发现还是LOOK算法好用。事实也证明,在绝大多数测试点,都能拿到97以上的高分,还有相当一部分达到了99+,动态调度+LOOK算法的组合几乎是性能杀手。但是遗憾的是,为了降低编写难度,减少代码量,尽可能防止bug出现,有一大部分内容我是做了阉割处理的,使得性能并未完全发挥,而且我在第三次作业中还有一个方法写错了,有些情况下根本不会给新增电梯分配指令,如果测试数据足够具有针对性,运行时长会达到算法正确时的1.5倍起步,2-4倍也不是不可能。

  还是那句话,虽然不是同一个时间,追求性能之心,人皆有之,但是绝不能本末倒置。性能,要的无非就是,简  单  有  效。

posted @ 2020-04-18 09:44  五年编程三年bug  阅读(195)  评论(0编辑  收藏  举报