电梯架构&优化相关
一、基于两种设计模式的电梯设计策略
我的整体思路就是将电梯作为一个可执行的最小个体来看待,它内部根据自己的私有队列有一个轻量级的方向和目的选择方法;而上层分配工作则交由调度器实现。
总体架构如下:
注:每一个Elevator都配有自己的单独的Queue。
生产者和消费者
在我的设计架构中,生产者和消费者这一设计模式是我设计的基石。而在三次作业中作业中,电梯都作为消费者出现,然而生产者却不尽相同。
在第一次作业中,输入线程直接作为生产者,而将输入存到一个以容器
ArrayList
(储存对象为Passage
类,即处理好的“产品”)为主要存储单元的缓冲区内,其中含有getIn
以及getOff
两种方法充当消费者从缓冲区取数据和清理缓冲区的“get”方法,而其中addPassage
则充当生产者将数据放入缓冲区的“put”方法。Input
、WaitingQueue
以及Elevator
三者构成了模型的生产者、缓冲区以及消费者。
而在第二、三次作业中,生产者变得模糊化了,并不直接由输入线程提供,而是由新加入的调度器线程充当生产者的模型,而除此之外,模型其他部分并没有什么改动,对电梯来说,它依然只是个面无表情的“吃人”机器,他不关心是谁提供了它的“食物”。
Worker Thread
使用这个模式其实是受了课上实验的启发,我在写的二次作业时完成了两版内容,第一版是完全由生产者消费者模型构成,写完后感到比较冗杂,也没有很好的满足低耦合的设计理念,主要是因为缓冲区(即调度线程和电梯队列)承担了过多的任务,让我感到它已经不是在单纯执行自己“份内”的工作了,这对于设计和迭代都是不利的。所以我在基层的电梯操作继续沿用生产者消费者架构的同时,引入了部分Worker Thread的模式。
我的设计是类似于实验课上的票务系统,输入端不停的投放生产物,存入第一个缓冲区总队列中,经由调度线程调度后,将总队列中人员分配给某一个电梯队列,之后每个电梯分别处理自己电梯队列中的任务即可。在相当于是来了一个Request,放入票池(也就是总队列),我的调度器充当了Channel的作用,然后我的worker(也就是电梯的私有队列)通过调度器获得Request,交给电梯进行处理。而在我代码的Worker Thread模式中,它不关心他的请求交出去后的处境。而且电梯不去要求任务而是等待分配。
Summary
单一来看其实两个模式都不是很适合于本次作业的完成,难以达到完全的高内聚和低耦合,也难以让实现线程之间协同和同步控制做的很好。而当我把两个模式和在一起时,他却体现出了较好的特性。下面从两个角度来看我选择设计策略的总体原因:
- 多线程的协同
我在第一次作业中只有两个线程,分别是电梯线程和输入线程,因为第一次作业其实就是在实现一个电梯内部的功能,故而我没有写调度器线程;而在二、三次作业中我加入了调度器线程,是为了保证电梯线程和输入线程的只实现自己的功能以及优化需求。我使用Worker Thread + 生产者消费者的设计模式,电梯线程和输入线程都只办自己的事,和其他两个线程并没有直接交互,他们分别通过电梯队列类和总队列类和调度器进行交互。而调度器则根据从两个队列中获取的信息进行调配,将总队列中的“任务”根据我的加权算法分配给某一个电梯队列,在适当的时候根据两队列情况进行sleep和对电梯线程进行notify,没有直接的交互保证了多线程的安全性(获取的是期望的输出),而且通过这样的形式,各个线程之间的协同变的更加清晰。调度器分别作为电梯的供给者和输入的消费者,协调着多个线程,作用域也非常明显(所对应的队列)。
- 同步控制
在第一次作业中同步控制非常简单,因为只有两个线程的交互,所以上好锁就完事了。而第二、三次作业中,我将我的模式拆分成两部分,也就是上文提到的生产者消费者&Worker Thread模式,这样同步控制也变的简单了,只要锁好电梯和输入,根据两种模式的要求分别针对调度器进行上锁即可。
二、架构和优化相关
SOLID
实际上当我们要着手编写代码时,一定要优先考虑架构(在多项式作业中任务驱动的我差点咽气),一个好的架构可以支持你的优化以及后续的拓展。下面从6大设计原则来看看设计架构如何规划。
SOLID原则 | 翻译 | 真实的翻译 |
---|---|---|
Single Responsibility Principle | 单一职责原则 | 电梯就该老老实实当电梯原则 |
Open Closed Principle | 开闭原则 | 加功能不要动我写好的电梯,更不要重构 |
Liskov Substitution Principle | 里氏替换原则 | 你要知道你用的是哪一个线程 |
Law of Demeter | 迪米特法则 | 电梯不要和其他线程直接沟通 |
Interface Segregation Principle | 接口隔离原则 | 能少交互就少交互 |
Dependence Inversion Principle | 依赖倒置原则 | ( |
这就是我开始写代码前的思考,依据这六大原则编写出的代码,会有较好的迭代和优化的空间
功能实现和性能优化
既然有了思路和约束,下一步就是功能实现和性能的优化啦。这很简单嘛这就需要我们考虑更细节的问题了。
- 功能实现
实际上,在观察过本次作业后发现,由于本次作业给出的时间范围较为宽泛,所以对于算法的要求确实不是很高,单纯的功能实现是比较容易的。我将功能实现分成两个部分:把人分配给每一个电梯;每一个电梯将自己队列中的人送到目的地。两部分具体的实现细节我都放在优化部分进行叙述。
第一次作业实际上就是整个专题中基本功能的实现---一个电梯如何完成自己的接人运送工作。我选择给每一个电梯配一个自己的队列,而队列里的人员就是就是等待这部电梯去接送的人员,为此我构造了一个Passage类,来存储请求的信息,以简化电梯队列的实现,并保证电梯只完成自己的工作不进行解码操作。而从迭代角度来看,我第二、三次作业基本没有更改我的第一次作业实现电梯功能的方法,只是在构造函数上和判断线程结束的地方略有修改。而二、三次作业改成多部电梯、楼层及人数限制,也并没有影响电梯的基础功能,只是引入了人员的分配问题,实际上就是调度器来协调电梯队列和总队列的事请,故而改动都集中在调度器类中。
- 性能优化
谈及优化的话,无非就是从两个方面来说:电梯运送自己队列中乘客的策略;总队列中人员如何进行分配。- 电梯运送自己队列中乘客的策略
在这一方面实际上我考量了很多方面的因素,最终确定了两个思路:Dijkstra算法,或是局部贪心。而在我做了形式化验证和随机数据检测后,最终我得出一个结论,局部贪心就可以了,因为数据的不可预知性导致Dijkstra算法的实际意义不大。这里介绍一下我的贪心策略:电梯在到达一层后自动检索自己的队列,找到最近的目标(电梯内的人就是目的地,电梯外的人就是始发地),前往接人或送人,这样可以保证折返历程尽量小。下图是一个简单的例子:
- 电梯运送自己队列中乘客的策略
主要代码段:
for (Passage p : people) {
if (p.getInOrout()) { // 人在电梯里
disc = Math.abs(now - p.getGoal());
if (min > disc) {
min = disc;
goal = p.getGoal();
}
} else if (!p.getInOrout()) { //人还没上电梯
disc = Math.abs(now - p.getLoc());
if (min > disc) {
min = disc;
goal = p.getLoc();
}
}
}
而电梯每一层都检索都要是否上下人,这样以应对不可预知的请求,完成捎带的策略。经过实际测试这样的方式对于单个电梯来说是比较优秀的方法,在第一次作业的强测里得到了99+的分数。
* 总队列中人员如何进行分配
这个地方的分配策略我思考了很久,因为涉及到两方面因素,每个电梯是否物尽其用以及人员是否适合某个电梯去处理。最终我构造了一个按权重分配人员的方法,由电梯内人数和电梯目前状态两方面决定。当然,还有一个初始化的过程,就是如果有空的电梯队列,优先放入空队列,保证电梯不空转。
主要代码段:
int min = 20;
int dis;
int noResponse = 0;
int temp = 0;
for (int i = 0; i < elevatorQueues.size(); i++) {
dis = Math.abs(elevators.get(i).getNowFloor() - p.getLoc()); \ 计算里程
synchronized (elevatorQueues.get(i)) {
if (elevatorQueues.get(i).getPeople().size() > 4) { \人数过多则对应电梯不响应
noResponse++;
continue;
}
}
if (dis < min) {
min = dis;
temp = i;
}
}
if (noResponse < elevatorQueues.size()) {
moveToEle(p, temp); \将人员移入电梯队列
return;
}
noResponse = 0;
for (int i = 0; i < elevatorQueues.size(); i++) { \如果所有电梯人数都超过5人则去除人数权重
dis = Math.abs(elevators.get(i).getNowFloor() - p.getLoc());
if (elevatorQueues.get(i).getPeople().size() == 7) {
noResponse++;
continue;
}
if (dis < min) {
min = dis;
temp = i;
}
}
if (noResponse < elevatorQueues.size()) {
moveToEle(p, temp); \将人员移入电梯队列
}
这是初版代码,实际上还可以增加电梯目前的方向这一因素,但是总体差别不大。而由于我个人的疏忽,导致在第二次作业中由于未适当休眠强测有两个CPUTLE,我简单计算了一下如果这两个点不错应该依然能够取得99+的分数。
而在第三次作业中,最显著的特点就是引入了乘客的感受,所以电梯内部单纯的贪心也变成了和时间因素相关的问题,所以我将原本的完全贪心,改成了根据等待时间增加优先级,当等待时间过长其优先级会提升并且目标会被锁定。
优化和设计的经验教训
其实三次作业的优化我都做的还算成功,但是在第三次作业中我犯了一个致命的错误,多删除了一个判断,还没有加上我原本打算放入的新方法。大概是因为我周四写了一半临时外出了导致的吧。不过这个问题一下子把我的性能分杀了个一干二净,原本我沿用了上次作业的方法,可是我在写完后忘了将请求的时间因素加入电梯队列,还忘了加入新电梯后打乱电梯队列。导致性能分骤降。这个问题让我意识到,每次作业都应该有一个形式化验证和自动数据的统计,算是一个比较大的教训吧。
建议大家在形式化验证式使用边界数据来得到运行轨迹看是否和我们期望的运行轨迹相符合