第二单元的问题是写一个关于电梯调度的程序。
需要模拟一个多线程实时电梯系统,从标准输入中输入请求信息,程序进行接收和处理,模拟电梯运行,将必要的运行信息通过输出接口进行输出。
主要锻炼学生的多线程程序编写能力。
由于需要实时的输入和输出,我们不得不采用多线程。
在这个单元中任务仍然被分为三个小任务:
①完成单电梯(随时允许输入)
②单电梯+(楼层增加负层,必须使用比先来先服务更加高效的算法)
③多电梯调度(增加重量限制、楼层停靠限制、换乘)
一、调度算法设计
单电梯的调度算法:
我在网上寻找调度算法后发现,网上大多的算法采用的都是静态算法,几乎没有动态算法。所以,我自己设计一个算法(或者说是参照实际使用中电梯的运转方式设计的算法),如下:
①查看该楼层是否有请求(包括进电梯和出电梯),有则开门转②,否则转③
② 让请求的人进出,并进行输出,所有请求进的人将这些人的出电梯请求同时加入电梯的请求序列中。关门转③
③沿电梯运动方向查看是否有任意类型的请求,如果有请求则向该方向运动一层转④,否则改变方向查看请求。如果在另一方向上有请求,则想这一方向移动一层转④。如果两个方向都没有请求则进程休息(wait),等待唤醒,唤醒后转④。
④如果不在输入任何需求则结束,否则转①。
多电梯的调度算法:
多电梯我觉得可以在原来的基础上进行修改,在调度器分配请求时通过某种方式将请求分给不同的电梯,然后每台电梯按照单电梯的调度算法进行运行即可。由于本人的多电梯的调度算法效率比较低,所以这里就不多赘述,可以参考其他同学的多电梯调度算法。
二、程序分析
对每次的程序使用MetricsReloaded进行oo度量,使用diagram描绘类图
重要符号意义说明:
- ev(G)基本复杂度是用来衡量程序非结构化程度的.
- Iv(G)模块设计复杂度是用来衡量模块判定结构,即模块和其他模块的调用关系。
- v(G)是用来衡量一个模块判定结构的复杂程度,数量上表现为独立路径的条数。
- LOC: Line of Code
- NCLOC:Non-Commented Line Of Code
P1
oo度量
LOC | NCLOC | 方法个数 | 属性个数 | |
Elevator | 183 | 169 | 5 | 2 |
Main | 9 | 9 | 1 | 0 |
Midlist | 26 | 26 | 5 | 2 |
OrderClass | 46 | 42 | 2 | 7 |
diagram类图
Sequence Diagram
P2
oo度量
LOC | NLOC | 方法个数 | 属性个数 | |
Elevator | 219 | 206 | 10 | 7 |
Main | 11 | 11 | 1 | 0 |
Midlist | 33 | 33 | 6 | 2 |
OrderClass | 46 | 62 | 2 | 5 |
diagram类图
Sequence Diagram
P3
oo度量
LOC | NLOC | 方法个数 | 属性个数 | |
Chart | 62 | 62 | 5 | 1 |
Elevator | 307 | 278 | 13 | 14 |
Entry | 8 | 8 | 1 | 0 |
Main | 12 | 12 | 1 | 0 |
Midlist | 367 | 322 | 21 | 19 |
Orderlist | 46 | 42 | 2 | 5 |
类图
Sequence Diagram
前面的时序图看着太复杂,而三次作业的结构大致一致,于是我做了一个简易版的sequence diagram。
优点:在第一次作业的时候就做好了基础的调度设计,是一个比较有效的设计,在随后的两次作业中修改的内容相对较少,也没有第一单元的重构情况,从类图上看,大体的结构并没有很大的变动。由于调度器不是一个进程,并发控制的难度比较低。
缺点:功能不够独立,在新需求出现时,常常要重新独立出一部分功能。设计时缺少借口等这类设计,导致一修改需求就会违背oo的设计SOLID原则。实际上第一次的设计从某种角度上来讲是存在瑕疵的,调度器不是一个进程,所以调度器在功能欲发复杂的情况下越来越庞大,但是却没有并行性,某种程度上限制了运行的性能。
三、多线程的协同和同步控制
在第一次作业中,需要考虑如何停下自己的程序,我采用的是让中间的管道传递一个状态,即输入端是否关闭,如果关闭则再电梯的所有请求处理完后停止进程。
在第二次作业中,实际上并没有太多的改变(因为我第一次就已经做了优化),但是这次需要避免cpu轮循的问题。那么我在之前的基础上补充电梯wait的要求,应该是输入队列里没有东西,而且输入进程没有停止。
在第三次作业中,为了继承之前作业的架构,我并没有将调度器作为一个独立的进程。但是由于调度器中可能没有属于电梯的请求,这个时候电梯就应该wait,但是就算输入结束了也不能轻易的结束进程,因为有些人可能需要换乘(但这个需求可能还没有出现)。所以我做了下面一个表来处理这个相对复杂的问题。
电梯可以获得的请求 | 输入端 |
有无分割的请求 (换乘请求) |
电梯动作 |
√ | - | - | 运行 |
× | √ | - | sleep |
× | - | √ | sleep |
× | √ | √ | 结束 |
四、分析自己程序的bug
第一次作业中,程序结束的控制计较容易出bug,因为在程序结束输入的时候(ctrl+D),仅凭借队列里是否有未服务的对象是不够的,还需要有一个判断标志,在电梯每次轮循后对输入是否结束进行判断,从而决定是否结束电梯线程。
第二次作业中,由于不能轮循(因为太占cpu时间),所以必须使用wait/notify方法,这个时候需要考虑好线程有没有可能会死锁,如何才能保证不死锁以及如何控制线程结束。(这三个问题是第二次作业中比较重点的bug/问题)
第三次作业,由于程序难度的增加,程序的复杂,轮循和,进程意外退出,进程没有被唤醒再次成为问题的关键。在这一次中,我最后应该将轮循和进程退出可能的情况进行列表推演,当当靠脑子想这件事情是很不靠谱的!
五、寻找他人bug(我不做互测)
多线程中,PV操作及其重要,关于线程之间的变量必须synchronize,可以着重看这一部分的代码,而且各种状态的传播也会在这一部分中,这一部分代码是最容易出bug的。
根据这次多线程的特点,应该着重考虑多线程的死锁、轮循这两个问题,着重分析他人代码中占用的位置。而且多线程的bug不一定能够触发,所以可能要多试几次。
相比于第一单元,bug的查找更加困难,因为有一些bug是概率性出现的,所以在测试中可以能需要对一个测试样例输入多次(或者选择不同的时间节点输入)。
六、SOLID原则分析
S.O.L.I.D是面向对象设计和编程(OOD&OOP)中几个重要编码原则(Programming Priciple)的首字母缩写。
SRP | The Single Responsibility Principle | 单一责任原则 |
OCP | The Open Closed Principle | 开放封闭原则 |
LSP | The Liskov Substitution Principle | 里氏替换原则 |
DIP | The Dependency Inversion Principle | 依赖倒置原则 |
ISP | The Interface Segregation Principle | 接口分离原则 |
SRP原则分析:在三次作业中,各个类的职责区分的非常清楚,启动线程、调度器线程、电梯线层、请求输入线程都能找到对应的类。
OCP原则分析:在这三次作业中(特别是第三次作业),我没有让三个电梯分别使用不同的策略(也就是说除了基础属性,其他他们都是相同的),所以不存在违背OCP原则的问题。
LSP原则、DIP原则分析:基于这次作业的难度,没有进行抽象和继承。
ISP原则分析:在设计的过程中,我并没有特别在意接口这个问题,所以我觉得这个单元的作业,在这个方面是有问题的。
七、心得体会
这次多线程的作业,让我全面理解了多线程。虽然在不需要涉及各种相对复杂的锁,但是仍然让我在大体上理解了多线程编写所需要的注意点。我希望接下来能够对我的编码进行指引,增加代码的限制,让我的代码更加工业化(能适应企业的要求)。