269
社区成员




在第一单元的表达式化简的学习中,我主要学习的是面向对象的一些基本的方法与思想;而第二单元的主题则是”多线程“这一我从未了解过的概念。经过一个月的学习,我对多线程也有了初步的理解,尽管在第二单元的强测中成绩非常不理想,但是我确实学习到了许多有关多线程的知识,也算是有所收获吧。
单线程:
从大一学习程序设计开始编写的所有程序,其实都是单线程的,也就是只有一个主线程:main()
函数。程序运行时会调用main()
函数作为程序入口,然后按顺序执行每一行代码。这在单步调试的过程中尤为明显:在main()
函数开头打一个断点,然后一直使用单步运行,程序就会一步一步运行,如果程序没有死循环的话就一定会在某处结束。对于同一个单线程程序,在环境与输入条件一致的情况下,程序的运行状况是可以确定的,也就是说,它是一个线性的运行过程,从程序入口开始,运行到主函数的第一个return
结束。
多线程:
多线程是什么呢,乍一看很难懂,但其实结合生活中的例子就可以很好地理解了。在《计算机组成原理》课程中,我们编写了一个简单的流水线CPU,这个CPU只能同时运行一段汇编程序。但从《操作系统》课程中我们可以学到,现代的操作系统都是支持多道程序的,这是因为现代CPU是很强大的,它可以给很多程序分配时间片,从CPU来看可能每一个时间点只在运行一个程序,但是从宏观角度看来,CPU可以同时运行多个程序(就像《三体》中的智子可以同时锁死多个粒子加速器一样,若其可以以超高速运动,那么从更加宏观的角度就可以认为其可以同时存在于多个地方)。此外,现在的CPU也早已不止一个核心了,故其同时可运行的程序更是巨量的(平时使用计算机时,我们也可以同时打开多个软件,多个网页同时运行)。
考虑一个简单的场景:
// case-1
int main() {
int array[10];
for (int i = 0; i < 10; i++) {
array[i] = i;
}
}
// case-2
int main() {
int array[10];
for (int i = 0; i < 5; i++) {
array[i] = i;
}
for (int i = 5; i < 10; i++) {
array[i] = i;
}
}
这两个程序的效率可以认为是几乎一样的,但我们注意到,case-2
中的两个for
循环其实是互不影响的,那么如果让这两个for循环同时进行,不久节约了一半时间了吗?这其实就是多线程的一种运用。
实际上,多线程的意义不只是节约时间,它使得程序设计增加了无数的可能性。在这个单元的学习中我发现,如果电梯调度没有多线程的帮助,将会难以实现。
java
程序使用java vm
来运行多线程程序,对应内核中的一个进程。
创建线程的方式有两种:
// 继承Thread类
public class TA extends Thread {
public void run() { // 执行入口点
this.go();
}
}
// 实现Runnable接口
public class TB implements Runnable {
public void run() { // 执行入口点
this.go();
}
}
一般而言,更加建议使用第二种方法,这是因为在java中,一个类只能继承一个类,但是却可以实现多个接口,因此,如果使用第二种方法的话就可以让这个线程类再去继承别的类。
但需要注意的是,第二种方法是实现接口,实例化了线程对象之后,不能直接调用start()
方法,因为该方法实现于Thread
类,具体需要操作如下:
// 实现了Runnable接口的TB类是不能直接调用start()方法的
TB tb = new TB();
// 需要创建一个Thread才可以start
Thread tbThread = new Thread(tb);
tbThread.start();
还需要注意,尽管这里使用start()方法的是tbThread而不是tb,但当我们需要把这个线程传递给某个别的线程时(可能另一个线程需要获取该线程的一些字段或者需要调用一些方法,比如在第七次作业中,分派器线程需要调用电梯线程的模拟时间方法,那么就需要在创建分派器线程时把电梯线程传递给分派器的构造方法),需要传递的是tb
而不是tbThread
,后者只用来启动,而不具有TB
类的各个字段和方法。
main()
函数作为程序的主线程,是程序的入口,在程序刚开始运行时就启动了。对于别的线程,start()
方法的执行是其启动的标志,每个线程其实和主线程一样,主线程是顺序执行main()
,而线程则是顺序执行run()
。
- 进入电梯前,乘客输入目标楼层;
- 系统根据设定的策略指定最合适的电梯来服务,将结果告知乘客;
- 乘客进入指定电梯后,无需再次按楼层按钮,电梯会自动将其送至目标楼层。
由于在本次作业中,每个乘客请求均指定了一部电梯负责接送该乘客。
,第五次作业比较简单,主要是为了熟悉多线程的一些方法和注意点。
第一次作业的基本模型如下:
InputHandler
和Elevator
充当生产者和消费者,RequestTable
就是托盘,即共享对象。生产者往托盘里放置产品,即请求。消费者从托盘获取产品进行对应处理。
由于第一次作业的请求会直接指定电梯,所以不需要分派器,输入线程直接把输入的请求发给对应的请求列表,电梯则根据请求列表运行即可。
在多线程编程中,可能会出现多个线程同时访问(读写)同一个对象的情况,这时候可能会产生未知的错误,我们需要使用线程同步来避免这种情况。最简单的线程同步就是保护一部分代码块,确保这部分代码块在同一时间只能由一个线程来访问。
java
中的synchronized
关键字用于实现线程同步,具有两种使用方法:
方法同步:
public class RequestTable {
public synchronized void method_1() {
// 方法体
}
public synchronized void method_2() {
// 方法体
}
}
在非静态方法加上该关键字,则可以保证对于同一个RequestTable
对象,同时最多只能有一个线程调用受synchronized
保护的方法。比如,线程1调用method_1
或method_2
时,其他线程不能调用method_1
或method_2
。
对象同步:
public class RequestTable {
private final Object lock_1 = new Object();
private final Object lock_2 = new Object();
public synchronized void method_1() {
synchronized (lock_1) {
// 代码块
}
}
public synchronized void method_2() {
synchronized (lock_2) {
// 代码块
}
}
}
在某一个代码块前面加上针对object
的锁,可以保证只有获取到该锁的线程才能访问该代码块。本质上,方法同步就是synchronized (this)
,这样子的话一个对象的所有方法都会共用一个锁,这不一定符合所有的需求,故可以使用对象同步来区分不同的锁。比如上面的例子中,两个方法用了两个不同的锁来保护,如此,一个线程调用method_1
时,其他线程不能调用method_1
,但不影响其他线程调用method_2
。
在大多数需求中,共享对象只具有”写-写“和”读-写“互斥,而支持”读-读“。此时,方法同步或使用普通锁的对象同步则不能支持”读-读“。这时候可以使用读写锁:在进入同步块前获取读写锁,在离开时释放读写锁。
public class RequestTable {
// 读写锁的构造方法可以接受一个布尔值,若为ture,则为公平读写锁;若为false或没有传值,则为写者优先锁
private final ReadWriteLock lock = new ReentrantReadWriteLock(true);
private final Lock readLock = lock.readLock();
private final Lock writeLock = lock.writeLock();
public void read() {
readWriteLock.readLock().lock();
try {
// read
} finally {
readWriteLock.readLock().unlock();
}
}
public void write() {
readWriteLock.writeLock().lock();
try {
// write
} finally {
readWriteLock.writeLock().unlock();
}
}
}
在生产者-消费者模型中,消费者需要在共享对象为空(或其中没有需要的产品)时等待生产者生产(需要的)产品,若使用while
循环来”轮询“的话,则会消耗大量CPU时间,因此需要引入wait()-notifyAll()
机制。
public class RequestTable {
private final boolean isEmpty;
public synchronized void addRequest() {
// add
isEmpty = false;
notifyAll();
}
public synchronized void waitRequest() {
while (isEmpty) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class consumer implements Runnable{
private final RequestTable requestTable;
public void run() {
while (true) {
if (!requestTable.isEmpty()) {
// 从共享对象获取产品
} else {
requestTable.waitRequest();
}
}
}
}
在这个例子中,消费者线程内有一个循环,如果当前共享对象内有其需要的产品,则获取之并进行一系列操作。若没有产品,则调用共享对象的waitRequest
方法。该方法内也有一个循环,在共享对象为空时wait()
并释放锁。当一个其他线程(比如生产者线程)调用共享对象的一个方法中使用notifyAll()
时,便唤醒消费者线程。
此外,wait()-notifyAll()
机制还可以用于别的场景,比如两个线程同步时间,这在第七次作业中有所涉及。
LOOK是最常用的电梯运行算法,它的优势是可以把电梯的运行分解为每一小步,这样电梯只需要关注当下的状态来决定下一步操作,本质是一种分治思想。LOOK算法具有很好的可扩展性,可以很好地进行迭代。原始的LOOK算法基本如下,也是第五次作业用到的:
public Type getAdvice(String curFloor, int curNum, boolean isUp,
HashMap<String, HashSet<PersonRequest>> destMap) {
// 判断是否需要开门
if (canOpenForIn(curFloor, curNum, isUp) || canOpenForOut(curFloor, destMap)) {
return Type.OPEN;
}
// 如果电梯里有人
if (curNum > 0) {
return Type.MOVE;
}
// 如果电梯里没人
else {
// 请求队列没人
if (requestTable.isEmpty()) {
if (requestTable.isOver()) {
return Type.OVER; // 如果输入结束,则电梯线程结束
} else {
return Type.WAIT; // 输入未结束,电梯进入等待状态
}
}
// 请求队列有人
else {
// 请求队列中有人的出发楼层位于电梯当前楼层的方向上
if (hasReqInDirection(curFloor, isUp)) {
return Type.MOVE;
}
// 请求队列中所有人都在反方向上
else {
return Type.REVERSE;
}
}
}
}
需要添加新的调度逻辑时(比如第六次作业的SHCE
和第七次作业的UPDATE
,只需要修改if-else
逻辑即可)
2023级的指导书多了一项新的要求:乘客优先级,需要我们尽可能安排高优先级的乘客更快抵达。
回想起来,在第二单元的作业中,我设想了多种方法来优化调度,但最终也没有和平均分配拉开差距,还消耗了很大的精力,虽然失败了,但也算学到了不少吧。
在第五次作业中,我进行了以下优化:在开门的时候,到达目的地的人先出去,然后让电梯内的人和想进来的人进行排序,优先让高优先级的人上电梯。
- 取消“乘客请求指定电梯”约束,需要自行设计电梯分配策略
- 新增“临时调度”请求;新增“RECEIVE”约束
- 修改乘客请求格式;修改“OUT”指令输出格式;修改测试数据约束;修改“实现提示”“提示与警示”部分
第六次作业主要有两项更改:临时调度请求、RECEIVE约束。
由于这次作业新增了调度体系,故改为双层模型,添加了一个主请求队列,通过分派器与请求队列进行交流。两组生产者-消费者分别是:
在收到临时调度请求时,InputHandlerThread
会直接通知对应电梯的RequestTable
(把请求列表的对应字段进行设置,策略类求取下一次电梯行动时可以优先根据是否有临时调度来决定怎么运行),这样就可以让电梯按照临时调度运行。
临时调度优先级高于一般的请求,故只需要修改Strategy
类,在其前面添加新的逻辑即可。
public Type getAdvice(String curFloor, int curNum, boolean isUp,
HashMap<String, HashSet<PersonRequest>> destMap) {
// 判断是否处于调度状态
ScheRequest scheRequest = requestTable.getScheRequest();
if (scheRequest != null) {
String toFloor = scheRequest.getToFloor();
if (toFloor.equals(curFloor)) {
return Type.SCHE_OPEN; // 到达调度请求的楼层,开门
}
else if (!FloorTool.isDirection(toFloor, curFloor, isUp)) {
return Type.REVERSE; // 电梯方向与调度方向不一致,则掉头
} else {
return Type.SCHE_MOVE; // 电梯方向与调度方向一致,则移动
}
}
// 判断是否需要开门
if (canOpenForIn(curFloor, curNum, isUp) || canOpenForOut(curFloor, destMap)) {
return Type.OPEN;
}
// 如果电梯里有人
if (curNum > 0) {
return Type.MOVE;
}
// 如果电梯里没人
else {
// 请求队列没人
if (requestTable.isEmpty()) {
if (requestTable.isOver()) {
return Type.OVER; // 如果输入结束,则电梯线程结束
} else {
return Type.WAIT; // 输入未结束,电梯进入等待状态
}
}
// 请求队列有人
else {
// 请求队列中有人的出发楼层位于电梯当前楼层的方向上
if (hasReqInDirection(curFloor, isUp)) {
return Type.MOVE;
}
// 请求队列中所有人都在反方向上
else {
return Type.REVERSE;
}
}
}
}
需要注意的是,本次作业具有RECEIVE的约束,需要考虑接受临时调度后不能再接受其他请求。但由于多线程不可预测性质,需要留出冗余(比如让电梯小睡一会再打印调度开始信息,防止RECEIVE信息在调度开始后打印)。
采用影子电梯进行分派,具体实现就是,复制电梯的各项属性,模拟电梯运行时间,根据时间长短来进行分派。
需要注意的是,即使找到运行时间最短的电梯也不一定要马上分派,这是因为有可能只是局部最优解,若找到的最短时长的电梯仍需要较长时间来完成请求,可以考虑等待一段时间后再分派。但具体“较长”是多少,则需要不断参数调整。
在第六次作业中,存在着“回退请求”到主请求队列的情况,处理不好会导致输入线程和分派器线程不知道何时结束(即使标准输入结束后,仍有可能有电梯回退请求),这一问题在第七次作业中新建了一个工具类来解决,而在本次作业中则回避了这个问题(不再回退请求)。
新增双轿厢电梯改造
修改"RECEIVE"约束;修改临时调度约束
大体架构与第六次作业一致,采用双层生产者-消费者模型。难点在于双轿厢改造中的线程同步与互斥,具体在下文分析。
在本次作业中,存在更新请求,需要把指定电梯修改为双轿厢电梯。输入线程的通知电梯的方式和上次作业的临时调度相似,直接给对应请求列表写入字段,影响策略类的行为。
但有一个难点:不同电梯状态不同,两台电梯不一定同时处于可改造状态(可能有一台电梯需要先放人出来再改造),这就需要线程同步了(即先进入可改造状态的电梯需要等待另一台电梯),我的处理是添加一个工具类DcController
public class DcController {
private int tryBeginNum;
private int tryEndNum;
public synchronized boolean tryBegin() {
tryBeginNum++;
if (tryBeginNum == 2) {
notifyAll();
return true;
} else {
notifyAll();
return false;
}
}
public synchronized boolean tryEnd() {
tryEndNum++;
if (tryEndNum == 2) {
notifyAll();
return true;
} else {
notifyAll();
return false;
}
}
public synchronized void waitForBegin() {
while (tryBeginNum < 2) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public synchronized void waitForEnd() {
while (tryEndNum < 2) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
这里设置了计数器,来记录是否两个电梯都处于对应的状态了;还设置了等待方法,供先到的那个电梯进行调用。
具体在电梯线程里如下调用:
if (!dcController.tryBegin()) {
dcController.waitForBegin();
// do something
} else {
// do something
}
// 开始结束
if (!dcController.tryEnd()) {
dcController.waitForEnd();
// do something
} else {
// do something
}
// 改造结束
这样子,先到达状态的线程会在if
条件中调用一次try()
,增加计数器,但由于返回值为false
而进入等待状态;后到来的线程同样调用try()
,增加计数器后由于计数已经到达2
,返回true
使之不进入等待状态,同时try()
方法唤醒先到的线程。这样便完成了同步。
主要问题围绕在换乘楼层:
// 普通电梯策略
public GlobalStateManager.Type getNormalAdvice(String curFloor, int curNum, boolean isUp,
HashMap<String, HashSet<PersonRequest>> destMap) {
// 判断是否处于改造状态
UpdateRequest updateRequest = requestTable.getUpdateRequest();
if (updateRequest != null) {
if (curNum > 0) {
return GlobalStateManager.Type.UPDATE_OPEN; // 开门放出乘客
} else {
return GlobalStateManager.Type.UPDATE_ACTION; // 进行改造操作
}
}
// 判断是否处于调度状态
ScheRequest scheRequest = requestTable.getScheRequest();
if (scheRequest != null) {
String toFloor = scheRequest.getToFloor();
if (toFloor.equals(curFloor)) {
return GlobalStateManager.Type.SCHE_OPEN; // 到达调度请求的楼层,开门
}
else if (!FloorTool.isDirection(toFloor, curFloor, isUp)) {
return GlobalStateManager.Type.REVERSE; // 电梯方向与调度方向不一致,则掉头
} else {
return GlobalStateManager.Type.SCHE_MOVE; // 电梯方向与调度方向一致,则移动
}
}
// 判断是否需要开门
if (canOpenForIn(curFloor, curNum, isUp) || canOpenForOut(curFloor, destMap)) {
return GlobalStateManager.Type.OPEN;
}
// 如果电梯里有人
if (curNum > 0) {
return GlobalStateManager.Type.MOVE;
}
// 如果电梯里没人
else {
// 请求队列没人
if (requestTable.isEmpty()) {
if (requestTable.isEnd()) {
return GlobalStateManager.Type.OVER; // 如果输入结束,则电梯线程结束
} else {
return GlobalStateManager.Type.WAIT; // 输入未结束,电梯进入等待状态
}
}
// 请求队列有人
else {
// 请求队列中有人的出发楼层位于电梯当前楼层的方向上
if (hasReqInDirection(curFloor, isUp)) {
return GlobalStateManager.Type.MOVE;
}
// 请求队列中所有人都在反方向上
else {
return GlobalStateManager.Type.REVERSE;
}
}
}
}
// 双轿厢电梯策略
public GlobalStateManager.Type getDcAdvice(String curFloor, int curNum, boolean isUp,
HashMap<String, HashSet<PersonRequest>> destMap, String transferFloor,
DcController.DcType dcType) {
// 判断是否需要开门
if (canOpenForIn(curFloor, curNum, isUp) ||
canOpenForOut(curFloor, destMap) ||
canOpenForTransfer(curFloor, destMap, transferFloor, dcType)) {
return GlobalStateManager.Type.OPEN;
}
// 如果电梯里有人
if (curNum > 0) {
return GlobalStateManager.Type.MOVE;
}
// 如果电梯里没人
else {
// 请求队列没人
if (requestTable.isEmpty()) {
if (requestTable.isEnd()) {
return GlobalStateManager.Type.OVER; // 如果输入结束,则电梯线程结束
} else {
return GlobalStateManager.Type.WAIT; // 输入未结束,电梯进入等待状态
}
}
// 请求队列有人
else {
// 请求队列中有人的出发楼层位于电梯当前楼层的方向上
if (hasReqInDirection(curFloor, isUp)) {
return GlobalStateManager.Type.MOVE;
}
// 请求队列中所有人都在反方向上
else {
return GlobalStateManager.Type.REVERSE;
}
}
}
}
依然使用等待-唤醒机制,同样在DcController
中实现
public class DcController {
private boolean isOccupied;
public synchronized boolean tryEnterTrans() {
if (!isOccupied) {
isOccupied = true;
notifyAll();
return true;
} else {
notifyAll();
return false;
}
}
public synchronized void waitForEnter() {
while (isOccupied) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public synchronized void exitTrans() {
isOccupied = false;
notifyAll();
}
}
电梯线程的移动方法需要添加条件
private void move() {
elevatorSleep(speed);
// 获取下一层
String nextFloor = isUp ? FloorTool.overFloor(curFloor) : FloorTool.underFloor(curFloor);
// 如果是双轿厢电梯
if (!dcType.equals(DcController.DcType.NON_Dc)) {
// 如果下一层是换乘楼层
if (nextFloor.equals(updateRequest.getTransferFloor())) {
//尝试进入换乘楼层,不成功则等待
while (!dcController.tryEnterTrans()) {
dcController.waitForEnter();
}
}
}
// 到达换乘楼层
//如果是双轿厢电梯
if (!dcType.equals(DcController.DcType.NON_Dc)) {
// 如果离开的是双轿厢楼层
if (curFloor.equals(updateRequest.getTransferFloor())) {
// 通知可能在等待进入的另一台电梯
dcController.exitTrans();
}
}
curFloor = nextFloor;
}
在这三次作业中,我的结束流程就是:
InputHandlerThread
结束后,通过MainRequestTable
通知DispatchThread
结束;DispatchThread
结束后,通过RequestTable
通知ElevatorThread
结束。但是有一个问题:InputHandlerThread如何知道何时结束呢?若存在请求回退(电梯或请求列表把请求回退到主请求列表),即使标准输入结束,仍有可能会有请求回退。故需要判断何时不会再有请求回退方可结束。
解决方法如下,添加一个GlobalStateManager
类来进行全局计数,当所有请求都被处理完时方可结束。
InputHandlerThread
收到请求后增加计数,OutputHandler
输出请求结束信息时减少计数。
public class GlobalStateManager {
// 记录未完成的请求数(包括所有请求类型)。
private static int globalRequestCount = 0;
private static final Object lock = new Object();
public static void addGlobalRequest() {
synchronized (lock) {
globalRequestCount++;
}
}
public static void removeGlobalRequest() {
synchronized (lock) {
globalRequestCount--;
if (globalRequestCount == 0) {
lock.notifyAll(); // 唤醒所有等待的线程
}
}
}
public static boolean allRequestEnd() {
synchronized (lock) {
return globalRequestCount == 0;
}
}
// 等待所有请求结束的方法
public static void waitAllRequestsEnd() throws InterruptedException {
synchronized (lock) {
while (!allRequestEnd()) {
lock.wait(); // 等待直到被通知
}
}
}
}
需要注意的是,对于静态方法,是不能进行synchronized
修饰的(静态方法不能对this
上锁),故这里需要使用显性锁来进行同步。
在这次作业中,调度器不仅需要等待输入线程生产,还需要等待存在电梯可以被分派(正在临时调度和更新的电梯无法接受请求,若所有电梯无法接受请求,则调度器需要等待),实现方法与线程结束类似。也在GlobalStateManager
中进行
public class GlobalStateManager {
private static int globalBusyCount = 0;
private static final Object busyLock = new Object();
public static void addGlobalBusy() {
synchronized (busyLock) {
globalBusyCount++;
}
}
public static void removeGlobalBusy() {
synchronized (busyLock) {
globalBusyCount--;
busyLock.notifyAll();
}
}
public static boolean isGlobalBusy() {
synchronized (busyLock) {
return globalBusyCount == 6; // 所有电梯都处于调度或升级状态
}
}
// 等待电梯调度或升级结束的方法
public static void waitGlobalBusyEnd() throws InterruptedException {
synchronized (busyLock) {
while (isGlobalBusy()) {
busyLock.wait(); // 等待直到被通知
}
}
}
}
关键在于,双轿厢电梯不一定能够到达目的地,怎么计算模拟时间?
public long testForTime(PersonRequest newRequest, boolean isNeedTestOther) {
// isNeedTestOther表示对双轿厢电梯测试时,是否考虑换乘后的模拟。
ShadowElevator shadowElevator = new ShadowElevator(
requestTable, DestMapClone(), UpQueueClone(), DownQueueClone(),
curFloor, curNum, isUp, newRequest);
if (dcType.equals(DcController.DcType.NON_Dc)) {
return shadowElevator.testToGetTime(speed, dcType, null);
} else {
long t1 = shadowElevator.testToGetTime(speed, dcType, updateRequest.getTransferFloor());
long t2 = 0;
if (isNeedTestOther && !reachable(newRequest.getToFloor())) {
PersonRequest transRequest = new PersonRequest(updateRequest.getTransferFloor(),
newRequest.getToFloor(), newRequest.getPersonId(), newRequest.getPriority());
t2 = dcController.getOtherElevator(dcType).testForTime(transRequest, false);
}
return t1 + t2;
}
}
尝试如上,对双轿厢电梯的另一台电梯再次进行换乘后模拟。
其实不一定好用,只是尝试而已,实际上好像也没有增加多少性能。
ElevatorThread | 2.69 | 9.0 | 35.0 |
---|---|---|---|
FloorTool | 3.0 | 4.0 | 12.0 |
InputHandlerThread | 3.0 | 5.0 | 6.0 |
Main | 4.0 | 4.0 | 4.0 |
OutputHandler | 1.0 | 1.0 | 6.0 |
RequestTable | 2.38 | 4.0 | 31.0 |
Strategy | 3.6 | 6.0 | 18.0 |
Strategy.Type | 0.0 | ||
Total | 112.0 | ||
Average | 2.54 | 4.71 | 14.0 |
每个类功能如下:
DispatchThread | 5.33 | 12.0 | 16.0 |
---|---|---|---|
ElevatorThread | 3.2 | 13.0 | 48.0 |
FloorTool | 3.0 | 4.0 | 12.0 |
InputHandlerThread | 3.0 | 5.0 | 6.0 |
Main | 4.0 | 4.0 | 4.0 |
OutputHandler | 1.125 | 2.0 | 9.0 |
RequestTable | 1.94 | 4.0 | 37.0 |
ShadowElevator | 4.375 | 11.0 | 35.0 |
Strategy | 4.0 | 9.0 | 20.0 |
Strategy.Type | 0.0 | ||
Total | 187.0 | ||
Average | 2.87 | 7.11 | 18.7 |
相比于上一次作业增加的类如下:
在第六次作业中,主请求队列和请求队列都是由RequestTable类来实例化的。
DcController | 1.63 | 2.0 | 18.0 |
---|---|---|---|
DcController.DcType | 0.0 | ||
DispatchThread | 4.0 | 7.0 | 16.0 |
ElevatorThread | 3.76 | 14.0 | 94.0 |
FloorTool | 3.2 | 4.0 | 16.0 |
GlobalStateManager | 1.375 | 2.0 | 11.0 |
GlobalStateManager.Type | 0.0 | ||
InputHandlerThread | 4.0 | 7.0 | 8.0 |
Main | 4.0 | 4.0 | 4.0 |
MainRequestTable | 1.125 | 2.0 | 9.0 |
OutputHandler | 1.1 | 2.0 | 11.0 |
RequestTable | 1.81 | 4.0 | 40.0 |
ShadowElevator | 5.11 | 13.0 | 46.0 |
Strategy | 5.42 | 11.0 | 38.0 |
Total | 311.0 | ||
Average | 2.77 | 6.0 | 22.21 |
在第七次作业中,电梯线程类变得非常臃肿,一度超过500行的风格限制,需要考虑分离功能到工具类。
第七次作业增加的类如下:
这里给出第七次作业中各个线程的协作关系:
第二单元的学习从成绩方面而言比较失败,但是确实学到了许多有关多线程的知识。遇到的困难一个一个解决也很有成就感。
最深刻的感受是多线程的不确定性,难以使用断点进行调试,甚至每次运行的结果都有所不同,确实需要更加全面的思想,尤其是对于如RECEIVE约束这种和运行顺序强相关的方面更加需要谨慎。
有点遗憾的是,针对时间指标的多个优化——影子电梯等,均没有达到理想的效果;此外,在这个单元的迭代中,未考虑电量指标的优化,确实是考虑不周。进行了长时间的优化最后却和平均分派所差无几,很是不甘心。