电梯作业总结
程序结构与复杂度的分析
第一次作业
1.设计思路
第一次作业是电梯作业的第一次,也是我多线程变成的第一次实践。任务是编写一个多线程实时电梯系统,采用FAFS的调度方式。由于第一次作业中没有涉及到多部电梯以及捎带的情况,因此来说是比较简单的。我采用的是指导书提示部分中的模式,即生产者消费者模式
- 主线程进行输入的管理,使用ElevatorInput,负责接收请求并存入队列
- 开一个线程,用于模拟电梯的活动,负责从队列中取出请求并进行执行,执行完后继续取继续执行
- 构建一个队列,用于管理请求,且需要保证队列是线程安全的
ElevatorInputHanler
在第五次的作业中,我将主线程main直接作为了输入管理进程,原因是看到了知道书中的主线程进行输入的管理,使用ElevatorInput,负责接收请求并存入队列
Request
在理解了题意和指导书中的模式推荐,我觉得这和我之前在《设计模式——可复用面向对象软件的基础》这本书中看到的生产者——消费者模式相近,故我采用了该模式来编写Request类
Elevator
Elevator类就是电梯类,内部保存着电梯的状态(如当前楼层),主要完成电梯在接收到请求之后的上下路以及开关门的操作
整体架构图如下:
2.度量分析
(1)复杂度分析
从图中可以看出在第一次电梯作业中,我的程序的复杂度在一个合适的范围,说明我第一次作业的设计是比较合理的
(2)类规模
在第一次作业中我的每个类的规模都在100行以内,比较合理
(3)时序图
第二次作业
1.设计思路
第二次作业中,电梯仍然是一部电梯,只是算法上用ALS捎带算法代替了FAFS的傻瓜调度,所以整体的架构我还是沿用第一次作业,仍然以生产者消费者模式为主。三各类的功能和第一次作业大体相同,只不过因为ALS算法而添加了不同的方法,在这里不过多赘述, 整体结构图如下:
2.度量分析
(1)复杂度分析
Elevator.deal()函数的代码如下:
private void deal() throws InterruptedException {
sleep(30);
queueRequest = request.getQueueRequest(nowFloor);
if (first) {
request.getAnother(direction, nowFloor);
}
if (queueRequest.size() >= 1) {
int i = 0;
boolean isOpen = false;
while (i < queueRequest.size()) {
if ((queueRequest.get(i).getFromFloor() == nowFloor
&& request.getFlag(i) != 0)
|| (queueRequest.get(i).getToFloor() == nowFloor
&& request.getFlag(i) != 1)) {
isOpen = true;
break;
}
i++;
}
if (isOpen) {
open(nowFloor);
}
i = 0;
while (i < queueRequest.size()) {
if (queueRequest.get(i).getFromFloor() == nowFloor
&& request.getFlag(i) != 0) {
movePeople(nowFloor, queueRequest.get(i).getPersonId(),
"IN");
if (request.getFlag(i) == 1) {
request.setFlag(i, 0);
}
i++;
continue;
}
if (queueRequest.get(i).getToFloor() == nowFloor
&& request.getFlag(i) != 1) {
movePeople(nowFloor, queueRequest.get(i).getPersonId(),
"OUT");
queueRequest.remove(i);
request.removeFlag(i);
continue;
}
i++;
}
if (isOpen) {
sleep(timeOpenOrClose * 2);
close(nowFloor);
}
queueRequest = request.getQueueRequest(nowFloor);
if (first) {
request.getAnother(direction, nowFloor);
}
}
}
首先,从代码中很容易看出里面包含了很多的if分支以及if分支的嵌套,这导致了v(G)的数值很大;并且在函数内部的request.getQueueRequest()函数是对Request实例方法的调用,并且在其中调用了很多的本类中的方法,增加了模块与模块之间的调用,所以iv(G)的数值高;总体的导致整体复杂度ev(G)的提高。
Request.getAnother()的代码如下:
public synchronized void getAnother(int direction, int nowFloor) {
if (!queue.isEmpty() && queue.get(0) != null) {
int i = 0;
while (i < queue.size()) {
if (queue.get(i) != null) {
if ((queue.get(i).getToFloor() - queue.get(i)
.getFromFloor()) * direction > 0
&& (nowFloor - queue.get(i).getFromFloor())
* (nowFloor - queue.get(i).getToFloor()) >= 0) {
queueRequest.add(queue.get(i));
flag.add(-1);
queue.remove(i);
continue;
}
}
i++;
}
}
}
从中可以看车里面的if分支的条件判断十分复杂,并且存在嵌套,这大大提高了程序调试的难度,我在第二次作业中出现的BUG也正是因为这个方法中的条件判断出现了问题。
Request.getQueueRequest()方法的代码如下:
public synchronized ArrayList<
PersonRequest> getQueueRequest(int nowFloor) {
while (queue.isEmpty() && queueRequest.isEmpty()) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
if (queueRequest.isEmpty()) {
queueRequest.add(queue.get(0));
flag.add(1);
if (queue.get(0) != null) {
queue.remove(0);
}
}
if (queue.size() >= 1 && queue.get(0) != null) {
for (int i = 0; i < queue.size(); i++) {
if (queue.get(i) != null) {
if ((queueRequest.get(0).getToFloor() - nowFloor)
* (queue.get(i).getToFloor() - nowFloor) >= 0) {
queueRequest.add(queue.get(i));
flag.add(1);
}
}
}
for (int i = 1; i < queueRequest.size(); i++) {
queue.remove(queueRequest.get(i));
}
}
return queueRequest;
}
从代码中可以看到,在这个方法中使用了Elevator类中的成员变量nowFloor,并且其中使用了大量的ArrayList类型的成员变量,这大大提高了这个方法的耦合度,导致了Elevator和Request两个类之间的依赖关系十分显著。此外,其中大量的if分支嵌套也大大提高了复杂度。
(2)类规模
从图中可以看出只有*ELevator**类的行数高于100,我觉得其中的原因主要是由于第二次作业是在第一次作业的架构上进行的功能的添加。这导致了Elevator类中由于捎带增加了很多方法,以及每个方法的代码量。
(3)时序图
第三次作业
1.设计思路
在第三次作业中,要求三部电梯进行调度,并且不同的电梯的可达楼层不一样,这不仅导致了多线程的线程安全问题,也导致了请求的分配问题。因为一个人的请求有可能需要通过换乘来处理。我的程序主要保证的是正确性,所以牺牲了很大一部分的性能。我解决这种情况的方法是,将这种请求分割为两个请求,以1层为界,因为所有的电梯都可以到达1层。
并且在整体的结构上,我这次采用的是课件上的Worker-Thread模式,相当于对之前的作业进行了重构。
- Main类是主类
- Request类是请求类,是对提供的PersonRequest类型的请求的一个封装,因为考虑到要对请求的起始楼层进行修改
- ClientThread类是输入类,负责将接收到的请求存储到Channel的请求队列中
- Channel类是整体的调度器,用来将请求跟据不同的情况分配到不同的电梯中,并且在这次作业中,我采用请求队列和调度器二合为一的方式
- WorkerThread类相当于前两次作业当中的电梯类
-整体程序的架构图如下:
2.度量分析
(1)复杂度分析
由于第三次作业中的Channel.getAnother()、Channel.takeRequest()和WorkerThread.deal()三个方法和第二次作业中的基本相同,故复杂度不在这里重复分析。
WorkerThread.moveElevator()方法的代码如下:
private void moveElevator(int floor, boolean doDeal)
throws InterruptedException {
while (nowFloor != floor) {
try {
if (nowFloor < floor) {
int temp = nowFloor;
for (int i = 1; i <= stopFloor.get(stopFloor.
indexOf(temp) + 1) - temp; i++) {
if (nowFloor == -1) {
nowFloor = 1;
i++;
} else {
nowFloor = temp + i;
}
sleep(moveTime);
arrive(nowFloor);
}
} else {
int temp = nowFloor;
for (int i = 1; i <= temp - stopFloor.
get(stopFloor.indexOf(temp) - 1); i++) {
if (nowFloor == 1) {
nowFloor = -1;
i++;
} else {
nowFloor = temp - i;
}
sleep(moveTime);
arrive(nowFloor);
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
if (doDeal && nowFloor != floor) {
deal();
if (requestQueue.isEmpty()) {
break;
}
}
}
deal();
}
从中可以很容易的看出导致ev(G)和v(G)两个复杂度数值大的主要原因就是内部的if分支的嵌套问题。
WorkerThread.run()
public void run() {
while (true) {
try {
sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (requestQueue.size() < maxNum) {
channel.takeRequest(requestQueue,
this.getName(), nowFloor, flag, maxNum);
}
if (requestQueue.size() == 1 && requestQueue.get(0) == null) {
break;
}
if (requestQueue.isEmpty()) {
continue;
}
try {
first = true;
direction = requestQueue.get(0).getFromFloor() - nowFloor;
logger.debug(direction);
moveElevator(requestQueue.get(0).getFromFloor(), false);
first = false;
Request another;
while ((another = dealFlag()) != null) {
direction = another.getToFloor() - nowFloor;
moveElevator(another.getToFloor(), true);
}
direction = requestQueue.get(0).getToFloor();
moveElevator(requestQueue.get(0).getToFloor(), true);
while (!requestQueue.isEmpty() && requestQueue.get(0) != null) {
if (flag.get(0) == 1) {
direction = requestQueue.get(0).getFromFloor();
moveElevator(requestQueue.get(0).getFromFloor(), true);
} else {
direction = requestQueue.get(0).getFromFloor();
moveElevator(requestQueue.get(0).getToFloor(), true);
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
与前面的moveELevator()相比,除了因为if分支导致的结构复杂以外,这个函数中由于调用了Channel类的实例channel的方法,导致了两个类之间的调用关系复杂,依赖程度高,提高了程序的耦合度,而且作为一个线程的run()方法,这样的高复杂度很容易导致线程安全的问题
(2)类规模
(3)时序图
程序BUG的分析
在这三次作业的强测和互测阶段,我一共只遇到了一种BUG。这个BUG体现在输出中就是一个人还没有进电梯就从电梯里面出来了。我遇到这个BUG是在第二次电梯作业的强测阶段,一共有4个测试用例测出了我这个BUG,导致我自己差一点没进强测。
经过分析,我发现这个BUG出现的原因是在捎带请求的处理上,出问题的地方是Request类中:
public synchronized void getAnother(int direction, int nowFloor) {
if (!queue.isEmpty() && queue.get(0) != null) {
int i = 0;
while (i < queue.size()) {
if (queue.get(i) != null) {
if ((queue.get(i).getToFloor() -
queue.get(i).getFromFloor()) * direction >= 0
&& (queue.get(i).getFromFloor() - nowFloor) /***/
* direction >= 0 /***/
&& (queue.get(i).getToFloor() - nowFloor) /***/
* direction >= 0 /***/
&& direction != 0) { /***/
requestQueue.add(queue.get(i));
flag.add(-1);
queue.remove(i);
continue;
}
}
i++;
}
}
}
出问题的部分就是我在代码中标注出来的。这一部分的条件一开始我没有加上。我处理捎带请求的思路是,我的电梯刚开始向着主请求的起始楼层运动,在这个过程中,如果有一个请求的运行方向和我当前电梯运行方向一样,我就捎带。但是,我最初没有加上一个判断条件:这个捎带请求的起始楼层必须是还未到达。这就导致了我实际上在队列中存储了一个请求作为捎带请求,但是没有输出这个人进入电梯的消息,最终导致了BUG的产生。
互测
自动评测
在这三次的互测过程中,我采用的都是自动化评测的方式。
我的测评机主要分为一下几个部分:
- 数据生成器
- 定时输入数据
- 结果检查
数据生成器
- N:请求总数,通过randint声称
- personId:人员Id,从0开始递增
- Floor:一个列表,里面存储着电梯能够到达的楼层
- Time:时间,起始值为0.0
- TimeAdd:一个列表,Time的增长幅度,初值为[0.0, 0.1]
每一次循环,使用随机数来选择0.0或0.1作为时间的增长
用随机数来选择起始楼层和终 止楼层,如果选择的楼层一样则重新选择直到不同为止
将请求输出到文件中
定时输入数据(JAVA)
- DealClass
- InputClass
- Instruction
- MainClass
- OutputClass
InputClass
返回一个String[]数组,内部保存着所有的输入
Instruction
- 指令类
- 内部有time(double)和ins(String)两个属性
- time属性是请求的时间
- Ins属性是请求
- 构造方法
Public Instruction(double time, String ins) {
this.time = time;
this.ins = ins;
}
DealClass
- 解析输入的请求
- 通过正则表达式提取请求中的时间和请求本身
- 使用instruction的构造方法
- 返回一个Instruction[]数组
OutputClass
- 输出线程
- 当前时间now初始为0
- 对于每一个请求,通过sleep的方式控制输出的前后
Thread.sleep((long)(time * (ins[i].getTime() - now)));
Now = instruction[i].getTime();
结果检查(python)
- 初始化一个trueRequest字典和outRequest字典
- trueRequest通过解析输入文件,得到每个人的真正请求
- outRequest通过解析输出文件,得到每个人在电梯运行之后实际执行的请求
- PersonId作为键值,[fromFloor, toFloor]作为元素,通过对比字典的每一项,如果相同就说明结果正确
Batch命令
javac -cp elevator-input-hw3-1.4-jar-with-dependencies.jar;timable-output-1.1-raw-jar-with-dependencies.jar src/*.java -d src/class
javac -cp elevator-input-hw3-1.4-jar-with-dependencies.jar;timable-output-1.1-raw-jar-with-dependencies.jar TestClass/src/*.java -d TestClass/class
set /a a = 0
:start
echo %a%
data.py
type in.txt |java -classpath TestClass/class MainClass |java -classpath elevator-input-hw3-1.4-jar-with-dependencies.jar;timable-output-1.1-raw-jar-with-dependencies.jar;src/class MainClass > dzh.txt
check.py
if %ERRORLEVEL% == 0 (
color 47
goto end
) else (
color 27
set /a a += 1
goto start
)
:end
结果展示
有效性
与第一次作业不同,我认为在本次评测过程中,自动化评测是十分必要的。在第一次作业里面,如果你不会自动评测,通过手动构造样例最起码是能够评测的,但是这次作业如果没有自动化的方式,光定时输入一点就很难做到。要想能够模拟评测的机的定时输入方式,必须要采用自动化评测的方式才够准确。如果是单纯的靠手输入,是难以保障准确性的。
但是呢,我认为线程安全方面的BUG,阅读代码要比自动评测更有效。因为通过代码结构的分析,你就能找到这个程序里面哪些资源是互斥的,哪些线程之间需要同步关系,一旦你找到了其中的BUG,那么只需要简单的构造一组样例就可以hack别人。
总结
本次的三次电梯作业,让我对多线程有了一个从无到有的实践。相比第一次作业,我的程序的设计明显的有了提高,在这三次作业中,我使用过两个模式:生产者模式和Worker-Thread模式,这使得我的程序的模块的分工分明,大大降低了BUG修复过程中的困难。并且,在电梯作业的设计过程中,我对请求队列究竟放在电梯内部还是放在调度器中思考了很久,最终选择了放在电梯中。我认为这样的思考是一个很好的锻炼过程。
并且在这三次作业中我尝试了自己制作对拍器,也对batch命令有了更深的认识。
但是,在这三次电梯作业中我都没有去尝试对电梯的调度进行优化,在研讨课上听了大佬们的LOOK算法,觉得这一部分的缺漏是一个很大的需要补的地方。并且,对于多线程里面锁的使用,我都是直接将方法上了锁,而没有采用锁代码块或者采用阻塞队列这样更高效率并且更高端的操作,我认为这些都是在以后需要提高的。