BUAA OO 2024 第二单元总结

李卓远-21377153 学生 2024-04-20 18:45:35

前言

本单元的主要目标是开发一个多线程的实时电梯控制系统。初始阶段,我们需掌握Java的多线程编程技巧和线程安全的基础处理技术,以确保实现电梯的基本功能。随后,需要应用更高级的线程同步与互斥手段来引入reset功能,并通过调整和改善电梯调度逻辑,以优化其性能。整体来看,本单元的核心挑战在于有效管理线程的同步与互斥,并且制定出一个高效的电梯调度方案。本文将概述本单元三次作业的核心内容,包括线程安全的策略、线程间的协作方式以及电梯的调度策略分析。

hw5

hw5需要实现基本的电梯运行功能。

UML类图与UML协作图

img

img

同步块的设置和锁的选择

在本单元中,我采用生产者-消费者模式进行设计。其中,生产者角色由输入线程InputThread承担,消费者角色则由6个Elevator线程组成。具体的操作流程包括:InputThread首先将接收到的乘客请求放入公共等待队列waitAll中,随后,调度器线程Schedule将waitAll队列中的请求按照一定的策略分配到每个Elevator各自的等待队列waitQueue中。

在作业hw5中,主要的考虑是解决inputThread线程写入waitAll和manager线程读取waitAll之间的冲突,以及Schedule线程写入各Elevator的waitQueue与各Elevator读取其waitQueue之间的冲突。为了处理这些冲突,我为WaitQueue类的相关读写操作添加了synchronized关键字,以确保线程安全。此外,对于那些需要频繁访问WaitQueue类的代码块,我也使用了synchronized关键字进行了同步控制。

具体到代码,对WaitQueue的增加Request以及读取Request的方法如下:

public synchronized void addRequest(Request person) {
    waitQueue.add(person);
    notifyAll();
}

public synchronized Request getRequest() {
    if (!isEnd() && isEmpty()) {
        try {
            wait();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    if (!waitQueue.isEmpty()) {
        Request request =  waitQueue.remove(0);
        notifyAll();
        return request;
    }
    return null;
}

关于锁与同步块中处理语句之间的关系,为了使得同步块范围尽量小,同时保证线程的安全性,需要确保锁和同步块中执行的代码语句有强相关性,例如waitQueue锁住的代码块里面要尽量都是读写waitQueue的语句。并且要注意锁的对象是没有传递关系的。

这一点我一开始没有搞清楚导致踩了坑,例如Elevator中实现的getIn方法,我一开始误以为既然waitQueue作为Elevator的一个属性,那么对Elevator加锁就能保证对waitQueue的操作是线程安全的,因此直接在方法上加了synchornized关键字,但是实际上这样做是不对的,因为这样只能保证对Elevator的操作是线程安全的,而对waitQueue的操作并不是线程安全的,因此应该在对waitQueue进行操作的代码块上加锁。

    public boolean getIn() {  // 判断是否有人要进电梯
        synchronized (waitQueue) {
            if (!waitQueue.isEmpty() && insideList.size() < 6) {
                for (Request r : waitQueue.getWaitQueue()) {
                    if (onTheWay(r, direction) && r.getStart() == currentFloor) {
                        waitQueue.notifyAll();
                        return true;
                    }
                }
            }
            waitQueue.notifyAll();
            return false;
        }
    }

调度器及调度策略

调度器由Schedule类进行实现,它通过waitAll和InputThread与Elevator进行交互,由InputThread向waitAll中写入数据,Schedule从中取出后读取数据,然后按照调度策略分配给相应电梯的waitQueue。hw5的乘梯请求中指定了每个请求的电梯号,只需要将请求分配给对应的电梯即可。

bug分析

本次作业强测和互测均未出现bug。

hw6

本次作业在第一次的基础上增加了电梯的RESET指令。处理RESET指令时,我采取的策略是将被reset的电梯内乘客及被reset的电梯的等待队列中所有的乘客请求重新加入到waitAll中进行再分配,并将电梯硬控1.2秒后再重新运行。然而实现思路虽然不难,但实际实现起来却有较大难度。为了更好地解决线程冲突问题,我引入了读写锁ReentrantReadWriteLock来进行更精确高效的同步控制。

另外,由于电梯随时都有可能被reset并将自己的相关乘梯请求放回到waitAll中,故hw5中通过InputThread的结束判断Schedule和Elevator的结束是不完善的,需要判断是否满足所有发出请求的乘客都到达了目的地并且InputThread输入结束来判断Schedule和Elevator是否应该结束。

UML类图与UML协作图

img

img

同步块的设置和锁的选择

由于本次作业与上次作业在基本架构方面未发生改变,所以上次的同步控制块基本不用加以修改。主要修改的部分是对于Schedule类和InputThread类的控制,由于RESET指令需要修改电梯属性,因此在该类中加上了读写锁来保证线程安全,例如方法的具体实现如下:

public void addElevators(Elevator elevator) {
    lock.writeLock().lock();
    try {
        elevators.add(elevator);
        waitLists.add(elevator.getWaitQueue());
    } finally {
        lock.writeLock().unlock();
    }
}

除此之外,由于在Maintain之后需要修改被踢出乘客的出发地,因此Person也可能出现线程冲突,所以我在该类内部也加上了读写锁。

public int getStart() {
    lock.readLock().lock();
    try {
        return start;
    } finally {
        lock.readLock().unlock();
    }
}

调度器及调度策略

我采用较为简单的调度策略:对于所有不处于reset状态的电梯,如果乘客的目标方向与该电梯当前运行方向一致,并且该电梯还未到达等待乘客的出发层,且它的内部乘客和等待乘客之和不满容量上限,则将该电梯视作一个备选电梯,在所有备选电梯中选择一个离请求乘客楼层距离最近的电梯作为最终决定的电梯。如果所有电梯都不满足备选电梯的条件,那么就分配给内部和外部乘客数量之和最小的电梯,以尽量保证均衡性。如果目前没有符合条件的电梯,就继续等待。

这样的调度策略综合考虑了时间和电量的因素。时间方面,尽量均衡的去讲请求分配给各个电梯,以免出现少数电梯在处理大量请求,而其他电梯却空闲的情况。电量方面,尽量选择距离请求乘客楼层最近的电梯,并且同批次尽量接更多的乘客,以减少反复接送的电量消耗。这样的调度策略在实际运行中表现良好,能够有效的提高电梯的运行效率。

具体实现如下:

public WaitQueue getWaitQueue(Passenger passenger) {
    int start = passenger.getStart();
    int distance = 20;
    Elevator e = null;
    WaitQueue waitList = null;
    lock.readLock().lock();
    try {
        while (true) {
            while (waitList == null) {
                for (Elevator elevator : elevators) {
                    if (!elevator.isReset()) {
                        int direction = elevator.getDirection();
                        int currentFloor = elevator.getCurrentFloor();
                        int capacity = elevator.getCapacity();
                        WaitQueue waitQ = elevator.getWaitQueue();
                        synchronized (waitQ) {
                            if ((passenger.getDestination() - start) * direction >= 0 &&
                                    (waitQ.getSize() + elevator.getInsideNum() < capacity) &&
                                    Math.abs(currentFloor - start) < distance) {
                                if (direction == 0 || (direction == 1 && currentFloor <= start)
                                        || (direction == -1 && currentFloor >= start)) {
                                    distance = Math.abs(currentFloor - start);
                                    waitList = waitQ;
                                    e = elevator;
                                }
                            }
                        }
                    }
                }
                if (waitList == null) {
                    int min = 200;
                    for (Elevator elevator : elevators) {
                        WaitQueue waitTemp = elevator.getWaitQueue();
                        if (elevator.isReset() || waitTemp.getSize() > 30) {
                            continue;
                        }
                        int insideNum = elevator.getInsideNum();
                        synchronized (waitTemp) {
                            int outsideNum = waitTemp.getSize();
                            if (insideNum + outsideNum < min) {
                                waitList = waitTemp;
                                min = insideNum + outsideNum;
                                e = elevator;
                            }
                        }
                    }
                }
            }
            synchronized (e) {
                if (!e.isReset()) {
                    TimableOutput.println("RECEIVE-" + passenger.getId() + "-" + e.getID());
                    break;
                }
            }
        }
        return waitList;
    } finally {
        lock.readLock().unlock();
    }
}

bug分析

本次作业强测未出现bug。但互测中被hack出TLE的bug,因为在对电梯进行reset操作时,会取出其相关的所有乘客重新加入到总队列中,如果电梯运行一段时间后,突然5个电梯都进行reset,就会导致剩下的一个电梯被塞满请求,而其他电梯完成reset后空置,从而TLE。解决方法倒也很简单,即限定电梯的等待队列人数不得超过30人,否则请求将继续等待直至有合适的电梯接纳。

hw7

本次作业在第二次的基础上增加了双轿厢电梯电梯的RESET指令。为了尽量减少改动量(那周实在忙(悲)),本次作业我主要是在普通电梯的基础上进行改造以实现双轿厢电梯的功能。思考了一下发现完全可以把双轿厢电梯沿用普通电梯的方法将进行处理,只需要规定上下轿厢只允许一个处在运行状态,并当电梯在转换层运行时进行上下轿厢的乘客转让,并且在转换层加入让层机制以防止碰撞即可(下文会详细阐述)。

我也构思了理论上性能更高的方法,但因为时间原因没有加以实现。即出现双轿厢电梯reset请求时,将被reset的普通电梯分裂为两个电梯加入到总电梯队列中,并将电梯的运行范围作为参数,以让上下两个轿厢作为独立的电梯去运行。这样在乘客发出请求之后,每个电梯都能同时运行接送乘客,如果一次不能到达,则到达转换层之后重新发出请求等待接送即可(一个乘客可能被转送多次),这样理论上可以提高电梯的运行效率。至于防撞机制,可以对于转换层设置一个值为1的信号量,从而控制只允许一个同单位电梯进入转换层且到达后需要尽快离开该层,另一个电梯则需等待。

UML类图与UML协作图

img

img

同步块的设置和锁的选择

本次作业与上次作业在基本架构方面也未发生改变,只是在Elevator中增加实现了双轿厢电梯的运行机制,所以上次的同步控制块基本不用加以修改。

调度器及调度策略

本次作业由于对运送乘客的要求与第二次基本一致,故调度策略基本没有改变(普通电梯和双轿厢电梯视为同等地位)。

防撞机制

本次作业对于双轿厢电梯,需要控制转换层不能同时出现AB轿厢电梯,因此需要实现防撞机制。我的实现方法主要基于逻辑判断,虽然繁琐且不太优美,但至少保证了正确性:上/下电梯到达转换层完成必要的人员进出之后,需要立刻让出该层,以便另一个电梯进入。另外进入转换层前,需要检查转换层中是否有电梯,如有则让其让出(不过这种情况其实不会出现,由于人为限制了上下轿厢只有一个处于运行机制,所以理论上不会出现一个电梯想进入转换层时需要等待另一个电梯让层的情况)具体的代码实现如下:

private void doubleDown() throws InterruptedException {
    if (upperCurrentFloor == currentFloor) {  // 如果上电梯在运行
        if (upperCurrentFloor > transFloor + 1) {  // 没到达转换楼层正常运行
            upperCurrentFloor--;
            currentFloor--;
            Thread.sleep(speed);
            TimableOutput.println("ARRIVE-" + upperCurrentFloor + "-" + id + "-B");
        } else if (upperCurrentFloor == transFloor + 1) {
            currentFloor--;
            if (transFloor == lowerCurrentFloor) {
                lowerCurrentFloor--;
                Thread.sleep(speed);
                TimableOutput.println("ARRIVE-" + lowerCurrentFloor + "-" + id + "-A");
            }
            upperCurrentFloor--;
            Thread.sleep(speed);  // 到达转换楼层
            TimableOutput.println("ARRIVE-" + transFloor + "-" + id + "-B");
            TimableOutput.println("OPEN-" + transFloor + "-" + id + "-B");//上厢放人
            sleep(200);
            ArrayList<Passenger> newList = new ArrayList<>();
            boolean trans = transDouble("-B", "-A", newList);
            if (!trans) {
                PassengerInTrans("B");
                direction = 1;
            }
            sleep(200);
            TimableOutput.println("CLOSE-" + transFloor + "-" + id + "-B");
            upperCurrentFloor++;
            Thread.sleep(speed);  // 上轿厢让层
            TimableOutput.println("ARRIVE-" + (transFloor + 1) + "-" + id + "-B");
            if (trans) {
                while (transFloor != lowerCurrentFloor) {  // 下轿厢到转换层
                    lowerCurrentFloor++;
                    Thread.sleep(speed);
                    TimableOutput.println("ARRIVE-" + lowerCurrentFloor + "-" + id + "-A");
                }
                TimableOutput.println("OPEN-" + transFloor + "-" + id + "-A");
                sleep(200);
                for (Passenger person : insideList) {
                    TimableOutput.println("IN-" + person.getId() + "-" +
                            transFloor + "-" + id + "-A");
                }
                PassengerInTrans("A");
                sleep(200);
                TimableOutput.println("CLOSE-" + transFloor + "-" + id + "-A");
                lowerCurrentFloor--;
                Thread.sleep(speed);
                TimableOutput.println("ARRIVE-" + lowerCurrentFloor + "-" + id + "-A");
                currentFloor--;
            } else {
                currentFloor++;
            }
        }
    } else if (lowerCurrentFloor == currentFloor) {
        lowerCurrentFloor--;
        currentFloor--;
        sleep(speed);
        TimableOutput.println("ARRIVE-" + lowerCurrentFloor + "-" + id + "-A");
    }
}

bug分析

本次作业出现bug,原因是在Elevator类的reset“踢人”过程中,过早的减少了电梯的insideNum值(还没有来得及将电梯内人员请求重新加入到总队列中),但这个值同样在Schedule类中被使用以判断运行是否结束,因此可能发生在乘客移交过程中调度器误判导致提前终止。将减少insideNum值的操作移至请求重新加入总队列之后,修复了该bug。

另外关于多线程的调试,由于多线程的特性,有时候会出现一些难以复现的bug,并且断点调试的方法也基本无法运用。这时就需要通过打印日志的方式来进行调试。在这次作业中,也是运用TimableOutput.println()大法以便更好地理解程序的运行过程,还是帮助我发现了不少问题。结合讨论区中的帖子,也是运用打印法解决了CTLE的轮询问题。

心得体会

本单元的任务迭代过程中,我的程序整体架构并没有发生太大的变化,这体现出生产者-消费者模型具有较强的扩展性,也说明了层次化设计的重要性。在第二三次作业的迭代中,如需增加电梯的新功能或者运行限制(如双轿厢电梯的引入),只需要修改Elevator类的相关方法;如果需要修改调度策略,则修改Schedule中的方法,整体架构没有像第一单元那样做了大改。另外对于增加的新功能和需求,课程组也帮助我们对InputThread中的请求进行了统一。对于未来的扩展能力也是如此,只需要修改对应的模块即可。

我认为在作业迭代中,稳定的内容是电梯的运行方式,例如上行、下行、开门、关门、上下乘客等实际上并没有发生较大改变,哪怕是双轿厢电梯的引入,我也是在普通电梯的基础上做了延伸和特判。易变的内容则是电梯的运行限制条件及状态,如RESET以及去年的MAINTAIN,这就要求做好类之间的协同,这也是多线程的主要难点。另外电梯的调度策略也是仁者见仁,智者见智,可以灵活的根据需求进行调整(虽然我没有做太多改动)。

本单元的主要难点还当是处理线程安全问题,理论课中介绍了wait()、notify()和读写锁两套方案。虽然作用基本相同,但在某些情况下还是有优劣之分的。如果一个代码段中需要多次访问一个共享的对象时,任何一个时刻丢掉锁都可能导致程序的异常执行,这种情况使用synchronized进行代码块的修饰就更加保险。所以说,这两者都是非常重要的,我们必须全部掌握,才能更好地解决线程安全的问题。

总的来说也是跌跌撞撞完成了久仰大名的电梯单元,虽然在实现过程中遇到了不少困难,但是通过不断的理论学习和代码实践,最终还是对多线程问题有了更深入的了解。在这个过程中,我学到了很多关于多线程编程的知识,也对Java的使用有了更深的理解。希望在接下来的学习中能够更好地应用这些知识,提高自己的编程能力。

...全文
46 回复 打赏 收藏 转发到动态 举报
写回复
用AI写文章
回复
切换为时间正序
请发表友善的回复…
发表回复
内容概要:本文围绕“阶梯碳下考虑P2G-CCS与供需灵活响应的IES优化调度”展开,基于Matlab平台构建综合能源系统(IES)在阶梯式碳交易机制下的优化调度模型。研究深度融合电制气(P2G)与碳捕集、利用与封存(CCS)技术,结合需求侧灵活响应机制,旨在提升系统的低碳运行能力与经济性。通过建立多能流耦合的优化模型,协调电力、天然气、热力等多种能源形式的协同调度,有效降低系统碳排放强度,并借助YALIMIP工具包调用求解器进行高效求解。文档提供了完整的代码实现、模型构建流程与结果分析方法,涵盖从问题建模到仿真实现的全过程,具备较强的可复现性与科研参考价值。; 适合人群:具备电力系统、能源系统或优化建模相关背景的研究生、高校教师及工程技术人员,尤其适合从事综合能源系统、碳减排策略、P2G与CCS技术集成研究的专业人员,需熟练掌握Matlab编程与基本的数学规划知识。; 使用场景及目标:①用于研究阶梯式碳交易政策下综合能源系统的低碳经济调度策略;②支撑P2G-CCS技术与需求响应机制在IES中的仿真集成与性能评估;③作为撰写高水平学术论文(如EI/SCI收录)的技术基础与复现资源,推动碳中和背景下能源系统优化方向的创新研究。; 阅读建议:建议结合百度网盘提供的完整代码与资料包,按照模块逐步调试程序,重点理解目标函数的设计逻辑、碳交易成本的建模方式、约束条件的数学表达及求解器的配置方法,同时关注多能耦合设备的建模细节,配合公众号“荔枝科研社”获取持续的技术支持与案例拓展。
内容概要:本文系统研究了基于卷积神经网络(CNN)与支持向量机(SVM)融合的CNN-SVM混合模型在数据分类预测中的应用,尤其聚焦于工业故障识别领域。通过Matlab平台实现,该方法首先利用CNN强大的多层次特征提取能力对原始输入数据进行深度特征学习,自动捕获关键局部模式与空间结构信息,随后将提取的高层特征作为输入传递至SVM分类器,借助SVM在高维空间中小样本条件下卓越的分类性能与泛化能力完成最终判别任务。文中详尽阐述了模型的整体架构设计、网络参数配置、训练优化流程及特征迁移机制,充分结合了深度学习在特征表达上的优势与传统机器学习在分类决策上的稳健性。实验部分通过实际故障数据集验证了该混合模型相较于单一CNN或SVM模型在分类准确率、鲁棒性和抗过拟合能力方面的显著提升,证明了其在复杂故障诊断任务中的有效性与先进性; 适合人群:具备一定机器学习与深度学习理论基础,熟悉Matlab编程环境,从事故障诊断、模式识别、智能制造、电力系统监控或工业数据分析等相关领域的研究生、科研人员及工程技术开发者; 使用场景及目标:① 应用于旋转机械、电力设备、航空航天等领域的多类别故障识别与状态监测;② 掌握深度特征提取与传统分类器融合的技术路径,提升小样本、高噪声环境下数据分类的精度与可靠性;③ 为撰写高水平学术论文、开展科研项目或工程实践提供可复现的算法框架与完整代码支持; 阅读建议:读者应深入理解CNN与SVM的协同工作机制,重点分析特征提取层与分类层之间的接口设计,建议动手运行并调试所提供的Matlab代码,尝试在不同数据集上进行迁移实验与参数调优,以全面掌握该混合模型的应用技巧与优化策略。

301

社区成员

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

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