- 设计策略
- 架构
- 工作流程
- 电梯流程
- 停靠流程
- 调度算法
- 电梯运行规则
- 任务分配规则
- 可扩展性分析
- 基于度量的代码分析
- 出现的BUG
- 心得体会
设计策略
架构
我这三次作业架构的差别不大,都是由以这三部分组成:输入部分,电梯,调度器。其中电梯和输入都是线程,有run()方法;调度器是共享变量,没有run()方法。下面是第一次作业的UML类图:
下面是第三次作业的UML类图,可以看见从第一次到第三次作业,除了多出一些方便换乘的类之外,结构没有多少变化。
调度器Manager在这个架构中承担了大部分的逻辑,包括电梯的状态由调度器维护,电梯的行为由调度器指导,请求由调度器统一分配给指定电梯。
电梯Elevator在这里只是个工具人,按照调度器的指示,sleep或者执行上人下人的操作。它自己不知道自己在哪一层,不知道自己能在哪层停靠,也不知道自己现在装了多少人。这一切由调度器统一控制,调度器保证分配给电梯的请求是电梯可以执行的。
输入类Input只是对官方的elevatorInput做了简单的封装,主要目的是识别EOF,以及第三次作业中请求的类型。
工作流程
电梯流程
电梯有五个状态:开门,关门,上行,下行,等待。调度器给出的指令实际上就是下一个状态,电梯根据下一个状态是什么执行相应操作。比如:下一个状态是“上行”,那么电梯就sleep(上行时间),sleep结束后请求下一个指令。
其中调度器发送指令的依据是电梯目前的状态信息,包括:电梯所处楼层,电梯目的地(下一个停靠楼层),电梯请求队列,电梯当前状态,电梯载人数量等。判断的核心逻辑是:比较电梯所处楼层和电梯当前目的地,决定上行下行,或者开门;如果没有目的地,就进入等待状态。
指令这部分逻辑很简单,但是电梯当前的目的地如何确定?电梯的目的地只会在请求发生变化时变化,所以只需要在请求变化的时候更新电梯的目的地即可。请求什么时候会发生变化?在新请求加入,电梯外的请求进入电梯,电梯内的请求离开电梯三种情况下请求会发生变化。这三种情况之外,电梯的目的地不可能发生变化。电梯的目的地要按照什么规则改变?这涉及到了我的电梯调度算法,在下一部分“调度算法”会说明。
停靠流程
电梯在执行开门/关门指令的时候,可以认为电梯在这层停靠。这里需要保证乘客在输出开关门之间的时间完成进入/离开电梯。
在快要关门的时候执行pick操作是为了尽可能避免来得晚的乘客错过这次电梯。
调度算法
本次作业我的调度算法很简单,但是强测分数证明这种算法效果尚可令人接受,三次强测分数分别是99+,97+,99+。
电梯运行规则
首先,调度器为每个电梯维护两个请求队列(实际上是数组,并没有先进先出的规则):已经进入电梯了的请求和等待进入电梯的请求。维护方式见下一部分“任务分配规则”。
新请求到来时,这个请求会进入 ”等待进入电梯“ 的队列。停靠过程中的pick操作会把请求从 “等待进入电梯” 的队列,转移到 “已经进入电梯” 的队列。乘客离开电梯会把这个请求从 “已经进入电梯” 的队列中移除。
调度算法采用了贪心的思想,每次请求队列发生变化(添加请求,乘客进入电梯,乘客离开电梯)的时候,获取所有请求需要停靠的楼层(电梯里面的请求需要在目的地停靠,电梯外面的请求需要在出发点停靠),选择距离当前楼层最近的请求作为下一个接/送的请求。接/送这个请求所需要去的楼层,就是下一个停靠的楼层,电梯不在这个楼层就不需要停靠。
这个贪心电梯只考虑眼前哪个任务离它最近,其它因素统统不考虑。
任务分配规则
第二次和第三次作业涉及到请求分配算法。
第二次作业由于电梯能在所有楼层停靠,所以采用平均分配的方式,新加入的请求会分配给当前请求数量最少的电梯(不是乘客数量,而是电梯内+电梯外的请求数量)。
第三次作业涉及到换乘和电梯类型,我的策略是给每个请求添加一个电梯类型的属性,表示这个请求只能由特定种类的电梯完成,然后在这类电梯中找一个请求数量最少的请求即可。换乘的实现方式是把需要换乘的请求拆成两个请求,每个请求由一个种类的电梯完成。在前半段的请求结束后(即乘客第一次下电梯),立即发出第二个请求。这样拆出来的两个请求都会当成普通请求完成。
可扩展性分析
我在本次三次作业的迭代中基本实现了开闭原则,三次作业全部使用的是第一次作业的架构,电梯的工作流程完全没有变化。
第二次作业主要变化是增加了多部电梯,所以我只是将原来调度器维护的电梯状态变量全部变成数组,再添加一个平均分配请求的方法就完成了,耗时很短。
第三次作业添加了电梯类型和换乘系统。我添加了一个请求解析类,作用是把一个需要换乘的请求拆成两个不需要换乘的请求,然后分别按照与第二次作业相同的方式进行处理。请求分配方式做了微调,从原来的所有电梯平均分配改成了在某个类型的电梯中平均分配,只是把for循环遍历对象从所有电梯变成了所有指定类型的电梯。
设计存在的问题是调度器类掌控着过多的信息,尤其是电梯的状态信息。电梯的信息可以放在电梯里面,或者独立出来,然后仅在电梯请求指令的时候交给调度器。调度器可以只承担一个分析计算的角色,而不用存储信息,这样可以进一步降低耦合。
基于度量的代码分析
第一次作业复杂度:
第二次作业复杂度:
第三次作业复杂度:
第三次作业中parsePersonRequest()方法的复杂度很高,这个方法的作用是分析请求能由哪个或者哪两个电梯完成。因为我换乘部分采取的是“大表格”法,即23*22种请求的情况全部列在一张表中,然后代码中使用if-else进行暴力判断。这种做法使得代码非常难以调试,也难以扩展。
所幸我没有出现BUG,而且这也是最后一次作业,所以不会再出现后续的迭代。
这种方法很多时候都不可取,但是在应对复杂但是后续扩展要求低的开发任务中,这也不失为一种快速且准确率高的方法。
出现的BUG
我在第三次作业的互测被测出来了一个BUG,出现原因是执行添加电梯操作之后我忘记notifyAll()了,导致了最后一条请求为电梯添加请求时,电梯会一直处在wait()状态而无人唤醒。
心得体会
其实这次作业整体来讲进行的还算很顺利,没出现什么致命的bug。但是我在这第一次多线程开发中,深深感觉到了多线程开发的复杂。这次作业中只用到了一小部分多线程的知识,我的架构也只是参考了生产者-消费者这一模型设计出来的。
在第一次作业的设计过程中,我连wait()和notifyAll()的使用都要参考课上实验中的代码,花了很长时间才设计出了一个合适的架构。后续的作业中也时沿着这个模型开发的,并没有尝试过其他架构。
虽然说我顺利完成了这次作业,并且成绩看起来还令人能令人满意,但是我知道我对多线程的学习只是刚刚跨出了第一步,我在以后的多线程开发中面对其它设计模式,一定还会感觉到陌生,一定还会出现很多难以发现的BUG。所以我以后每次坐电梯的时候都会提醒自己,要继续去虚心学习多线程的开发。