第二单元:多线程电梯

李元星-23371057 2025-04-16 08:32:52

第二单元:多线程电梯

概述

在第一单元的表达式化简的学习中,我主要学习的是面向对象的一些基本的方法与思想;而第二单元的主题则是”多线程“这一我从未了解过的概念。经过一个月的学习,我对多线程也有了初步的理解,尽管在第二单元的强测中成绩非常不理想,但是我确实学习到了许多有关多线程的知识,也算是有所收获吧。

(1)什么是多线程

  • 单线程

    从大一学习程序设计开始编写的所有程序,其实都是单线程的,也就是只有一个主线程: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循环同时进行,不久节约了一半时间了吗?这其实就是多线程的一种运用。

    • 实际上,多线程的意义不只是节约时间,它使得程序设计增加了无数的可能性。在这个单元的学习中我发现,如果电梯调度没有多线程的帮助,将会难以实现。

(2)Java中的多线程编程

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类的各个字段和方法。

(3)多线程的运行

main()函数作为程序的主线程,是程序的入口,在程序刚开始运行时就启动了。对于别的线程,start()方法的执行是其启动的标志,每个线程其实和主线程一样,主线程是顺序执行main(),而线程则是顺序执行run()

第五次作业

  1. 进入电梯前,乘客输入目标楼层;
  2. 系统根据设定的策略指定最合适的电梯来服务,将结果告知乘客;
  3. 乘客进入指定电梯后,无需再次按楼层按钮,电梯会自动将其送至目标楼层。

由于在本次作业中,每个乘客请求均指定了一部电梯负责接送该乘客。,第五次作业比较简单,主要是为了熟悉多线程的一些方法和注意点。

(1)生产者-消费者模型

第一次作业的基本模型如下:

img

InputHandlerElevator充当生产者和消费者,RequestTable就是托盘,即共享对象。生产者往托盘里放置产品,即请求。消费者从托盘获取产品进行对应处理。

由于第一次作业的请求会直接指定电梯,所以不需要分派器,输入线程直接把输入的请求发给对应的请求列表,电梯则根据请求列表运行即可。

(2)线程同步

在多线程编程中,可能会出现多个线程同时访问(读写)同一个对象的情况,这时候可能会产生未知的错误,我们需要使用线程同步来避免这种情况。最简单的线程同步就是保护一部分代码块,确保这部分代码块在同一时间只能由一个线程来访问。

synchronized关键字

java中的synchronized关键字用于实现线程同步,具有两种使用方法:

  • 方法同步:

    public class RequestTable {
        public synchronized void method_1() {
        // 方法体
        }
        
        public synchronized void method_2() {
        // 方法体
        }
    }
    

    在非静态方法加上该关键字,则可以保证对于同一个RequestTable对象,同时最多只能有一个线程调用受synchronized保护的方法。比如,线程1调用method_1method_2时,其他线程不能调用method_1method_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();
        }
    }
}

(3)等待-唤醒

在生产者-消费者模型中,消费者需要在共享对象为空(或其中没有需要的产品)时等待生产者生产(需要的)产品,若使用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()机制还可以用于别的场景,比如两个线程同步时间,这在第七次作业中有所涉及。

(4)LOOK算法

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逻辑即可)

(5)优先级优化

2023级的指导书多了一项新的要求:乘客优先级,需要我们尽可能安排高优先级的乘客更快抵达。

回想起来,在第二单元的作业中,我设想了多种方法来优化调度,但最终也没有和平均分配拉开差距,还消耗了很大的精力,虽然失败了,但也算学到了不少吧。

在第五次作业中,我进行了以下优化:在开门的时候,到达目的地的人先出去,然后让电梯内的人和想进来的人进行排序,优先让高优先级的人上电梯。

第六次作业

  1. 取消“乘客请求指定电梯”约束,需要自行设计电梯分配策略
  2. 新增“临时调度”请求;新增“RECEIVE”约束
  3. 修改乘客请求格式;修改“OUT”指令输出格式;修改测试数据约束;修改“实现提示”“提示与警示”部分

第六次作业主要有两项更改:临时调度请求、RECEIVE约束。

(1)双层生产者-消费者

img

由于这次作业新增了调度体系,故改为双层模型,添加了一个主请求队列,通过分派器与请求队列进行交流。两组生产者-消费者分别是:

  • 输入线程——主请求队列——分派器线程
  • 分派器线程——请求队列——电梯线程

(2)临时调度

在收到临时调度请求时,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信息在调度开始后打印)。

(3)分派策略

采用影子电梯进行分派,具体实现就是,复制电梯的各项属性,模拟电梯运行时间,根据时间长短来进行分派。

需要注意的是,即使找到运行时间最短的电梯也不一定要马上分派,这是因为有可能只是局部最优解,若找到的最短时长的电梯仍需要较长时间来完成请求,可以考虑等待一段时间后再分派。但具体“较长”是多少,则需要不断参数调整。

在第六次作业中,存在着“回退请求”到主请求队列的情况,处理不好会导致输入线程和分派器线程不知道何时结束(即使标准输入结束后,仍有可能有电梯回退请求),这一问题在第七次作业中新建了一个工具类来解决,而在本次作业中则回避了这个问题(不再回退请求)。

第七次作业

  1. 新增双轿厢电梯改造

  2. 修改"RECEIVE"约束;修改临时调度约束

大体架构与第六次作业一致,采用双层生产者-消费者模型。难点在于双轿厢改造中的线程同步与互斥,具体在下文分析。

(1)双轿厢电梯的改造

在本次作业中,存在更新请求,需要把指定电梯修改为双轿厢电梯。输入线程的通知电梯的方式和上次作业的临时调度相似,直接给对应请求列表写入字段,影响策略类的行为。

但有一个难点:不同电梯状态不同,两台电梯不一定同时处于可改造状态(可能有一台电梯需要先放人出来再改造),这就需要线程同步了(即先进入可改造状态的电梯需要等待另一台电梯),我的处理是添加一个工具类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()方法唤醒先到的线程。这样便完成了同步。

(2)双轿厢电梯的运行

主要问题围绕在换乘楼层:

  • 需要换乘的人到达换乘楼层需要下电梯
  • 进入换乘楼层时要考虑是否会相撞
策略类改动
    // 普通电梯策略
    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;
}

(3)线程结束

在这三次作业中,我的结束流程就是:

  • 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上锁),故这里需要使用显性锁来进行同步。

(4)调度器等待

在这次作业中,调度器不仅需要等待输入线程生产,还需要等待存在电梯可以被分派(正在临时调度和更新的电梯无法接受请求,若所有电梯无法接受请求,则调度器需要等待),实现方法与线程结束类似。也在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(); // 等待直到被通知
            }
        }
    }
}

(4)影子电梯改动

关键在于,双轿厢电梯不一定能够到达目的地,怎么计算模拟时间?

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;
    }
}

尝试如上,对双轿厢电梯的另一台电梯再次进行换乘后模拟。

其实不一定好用,只是尝试而已,实际上好像也没有增加多少性能。

架构分析

(1)第五次作业

Class metrics
ElevatorThread2.699.035.0
FloorTool3.04.012.0
InputHandlerThread3.05.06.0
Main4.04.04.0
OutputHandler1.01.06.0
RequestTable2.384.031.0
Strategy3.66.018.0
Strategy.Type0.0
Total112.0
Average2.544.7114.0
UML图

img

类功能

每个类功能如下:

  • Main:程序入口,实例化共享对象和线程,启动线程。
  • InputHandlerThread:输入线程,获取输入,把请求分派给各个RequestTable。
  • RequestTable:共享对象,存放请求列表。
  • ElevatorThread:电梯线程,从RequestTable获取请求,根据从Strategy获取的策略进行运行。
  • Strategy:策略类,绑定于每一个电梯线程,用于获取下一步的运行信息。
  • OutputHandler:输出工具类,包装各种输出信息。
  • FloorTool:楼层工具类,包装了常用的楼层相关计算函数。

(2)第六次作业

Class metrics
DispatchThread5.3312.016.0
ElevatorThread3.213.048.0
FloorTool3.04.012.0
InputHandlerThread3.05.06.0
Main4.04.04.0
OutputHandler1.1252.09.0
RequestTable1.944.037.0
ShadowElevator4.37511.035.0
Strategy4.09.020.0
Strategy.Type0.0
Total187.0
Average2.877.1118.7
UML图

img

类功能

相比于上一次作业增加的类如下:

  • DispatchThread:分派器线程,用于把主列表的请求按照分派策略分派给不同电梯对应的请求列表。
  • ShadowElevator:影子电梯,用于模拟电梯接受新请求后的运行时间。

在第六次作业中,主请求队列和请求队列都是由RequestTable类来实例化的。

(4)第七次作业

Class metrics
DcController1.632.018.0
DcController.DcType0.0
DispatchThread4.07.016.0
ElevatorThread3.7614.094.0
FloorTool3.24.016.0
GlobalStateManager1.3752.011.0
GlobalStateManager.Type0.0
InputHandlerThread4.07.08.0
Main4.04.04.0
MainRequestTable1.1252.09.0
OutputHandler1.12.011.0
RequestTable1.814.040.0
ShadowElevator5.1113.046.0
Strategy5.4211.038.0
Total311.0
Average2.776.022.21

在第七次作业中,电梯线程类变得非常臃肿,一度超过500行的风格限制,需要考虑分离功能到工具类。

UML图

img

类功能

第七次作业增加的类如下:

  • MainRequestTable:共享对象,主请求队列,在这次作业中把主次请求队列分开成了两个类。
  • GlobalStateManager:全局管理器,管理全局请求数和全局临时调度/更新,用于通知InputHandlerThread何时结束。
  • DcController:双轿厢电梯控制器:同步更新请求中两台电梯的开始更新和结束更新时间;防止双轿厢电梯运行中两台电梯在换乘楼层撞车。

(5)线程协作图

这里给出第七次作业中各个线程的协作关系:

img

Bug修复

(1)遇到的重大问题

  • 第五次作业:
    • 把电梯内的人移除电梯重新按照优先级排序时,忘记判断电梯内有没有人了,没人时还强行踹人,报了空指针错误。
  • 第六次作业:
    • 有大量的bug出现在RECEIVE约束方面,具体就是输出顺序等问题,对于难以衡量先后的线程问题,可以采用小睡一会的方式解决。
    • 无法正确判断线程结束条件,在这次作业中回避了回退请求的问题,在第七次作业中解决。
  • 第七次作业:
    • 影子电梯模拟前需要深克隆电梯的属性,相关方法未上锁,导致了迭代器错误,最终酿成了强测只有57分的悲剧。

(2)调试方法

  • 断点调试:对于多线程而言很难用,只在样例很简单且情况特殊时有奇效。
  • 日志打印:在程序中留下多个打印接口,能够很好地发现轮询与线程同步互斥问题。

感受与体会

第二单元的学习从成绩方面而言比较失败,但是确实学到了许多有关多线程的知识。遇到的困难一个一个解决也很有成就感。

最深刻的感受是多线程的不确定性,难以使用断点进行调试,甚至每次运行的结果都有所不同,确实需要更加全面的思想,尤其是对于如RECEIVE约束这种和运行顺序强相关的方面更加需要谨慎。

有点遗憾的是,针对时间指标的多个优化——影子电梯等,均没有达到理想的效果;此外,在这个单元的迭代中,未考虑电量指标的优化,确实是考虑不周。进行了长时间的优化最后却和平均分派所差无几,很是不甘心。

...全文
52 回复 打赏 收藏 转发到动态 举报
写回复
用AI写文章
回复
切换为时间正序
请发表友善的回复…
发表回复
Delphi 多线程电梯仿真系统附技术文档 某国际贸易中心共40层,设有载客电梯10部(用E0~E9标识)。利用多线程技术实现个电梯载客的仿真情况,另附上技术文档。   设计规则说明:   2.1 电梯的运行规则:   . E0、E1: 可到达每一层。   . E2、E3: 可到达1、25~~40层。   . E4、E5: 可到达1~~25层。   . E6、E7: 可到达1、2~~40层。   . E8、E9: 可到达1~~39层。   2.2 每部电梯的最大乘员量均为K人(K值可以根据仿真情况在10—20 人之间确定)。   2.3 仿真开始时,各电梯随机处于起符合运行规则的任意一层,为空梯。   2.4 仿真开始后,有N人(1000>N)在M分钟(10>M)内随机地到达该国际贸易中心的一层,    开始乘梯活动。   2.5 每个人初次所要到的楼梯层是随机的,令其在合适的电梯处等待电梯的到来。   2.6 每个人乘坐的合适电梯到达指定楼层后,随机地停留10—120秒后,在随机地去往另一    楼层,依次类推,当每个人乘坐过L次(L值可以根据仿真情况在3—10次之间确定)电梯后,第L+1次为下至底层并结束乘梯行为。到所有人结束乘梯行为时,本次仿真结束。   2.7 电梯运行速度为S秒/层(S值可以根据仿真情况在1—5之间确定),每个人上下的时间为T秒(T值可以根据仿真情况在2—10之间确定)   2.8 电梯运行的方向由先发出请求者决定,不允许后发出请求者改变电梯的当前运行方向,除非是未被请求的空梯。   2.9 当某层有人按下乘梯电钮时,首先考虑离该层最近的、满足条件2.8、能够最快到达目标层的电梯。   2.10 不允许电梯超员。

269

社区成员

发帖
与我相关
我的任务
社区描述
2025年北航面向对象设计与构造
学习 高校
社区管理员
  • Alkaid_Zhong
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告
暂无公告

试试用AI创作助手写篇文章吧