BUAA第二单元总结

杨荣津-21375077 学生 2024-04-20 12:38:26

一、 总结分析三次作业中同步块的设置和锁的选择,并分析锁与同步块中处理语句之间的关系

1. 主要锁的选择:

  • 主队列(processQueue)需要在输入处理线程(inputhandler)和调度器(scheduler)之间进行同步,以确保对请求队列的安全访问。因此选择主队列对象自身的内部锁来保护主队列的读写操作。
    public synchronized void addRequest(MyRequest request) {
            requests.add(request);
            notifyAll();
        }
    
  • 每个电梯的等候队列(requestQueue)也会涉及到读写互斥的问题,因为调度器会对requestQueue增加请求,电梯运行又会从requestQueue拿出请求,因此需要对每个电梯的requestQueue设置锁。
    public synchronized void addRequest(MyRequest myRequest) {
            int fromFloor = myRequest.getFromFloor();
            getSubRequests(fromFloor).add(myRequest);
            notifyAll();
        }
    

2. 同步块的设置:

  • 在输入处理线程(inputhandler)中,当将请求添加到主队列(processQueue)时,需要确保这个操作是原子性的,避免其他线程同时修改主队列导致数据混乱。在添加请求的代码块周围使用同步块,以确保每次只有一个线程可以修改主队列。
  • 在调度器(scheduler)中,当将主队列中的请求分配给每个电梯的请求队列时,也需要保证这个操作是原子性的,避免不同电梯线程同时修改请求队列。在这个操作的代码块周围使用同步块。

3. 锁与同步块中处理语句之间的关系:

  • 锁用于保护临界区,而同步块则指定了这个临界区。主要的操作都发生在添加请求到主队列和从主队列分配请求给每个电梯的操作中。因此,这些操作在同步块内执行,并且使用相应的锁来保护这些同步块,以确保线程安全性。

二、 总结分析三次作业中的调度器设计,并分析调度器如何与程序中的线程进行交互;总结分析三次作业中的调度策略,并分析自己的调度策略是如何适应时间、电量等多个性能指标的

  • 结合线程协同的架构模式(如流水线架构),分析和总结自己

    1. 三次作业架构设计的逐步变化和未来扩展能力画UML类图

      • 第一次作业

        img

      • 第二次作业

        img

      • 第三次作业

        img

      三次作业逐次迭代,其中必要重要的变化有以下几条

      1. 第一次到第二次的作业中,增加了Scheduler类。第一次作业中,由于输入会直接指定乘客需要乘坐的电梯,因此没有必要设置专门的调度类来把乘客分配给电梯。第二次作业中,不再指定需要乘坐的电梯,而是自定策略来把请求分配给电梯,以达到更好的性能;同时由于调度类的增加,也相应的需要增加一个ProcessQueue类,并且设置单例模式,作为请求未分配前的总请求队列。

      2. 第二次到第三次的作业中,增加了Controller类,因为前两次的作业中,电梯的运行逻辑直接在电梯中实现,包括了重置,开关门,进出乘客,运行。

        @Override
        public void run() {
            while (true) {
                if (currentPeople.isEmpty() && requestQueue.isEmpty() && !reset) {
                    if (processQueue.isRealEnd()) {
                        return;
                    } else {
                        elevatorWait(); } }
        
                if (reset) {
                    if (!currentPeople.isEmpty() && resetDepth != 0) {
                        resetDepth--;
                    } else if (currentPeople.isEmpty()) {
                        reset();
                    } else if (resetDepth == 0) {
                        display(Motion.OPEN);
                        goToSleep(openTimeConsuming + closeTimeConsuming);
                        currentPeople.release(currentFloor,elevatorId,true,processQueue);
                        display(Motion.CLOSE);
                        reset();
                    }
                }
        
                boolean mark1 = currentPeople.isSomeoneWantToOut(currentFloor);
                boolean mark2 = requestQueue.isSomeoneWantToIn(currentFloor, direction)
                        & (residualCapacity() != 0);
                boolean mark3 = currentPeople.isEmpty()
                        & !requestQueue.isSomeoneWaitingAhead(currentFloor, direction)
                        & requestQueue.isSomeoneWantToTurn(currentFloor, direction);
                if (mark1 || mark2 || mark3) {
                    display(Motion.OPEN);
                    goToSleep(openTimeConsuming + closeTimeConsuming);
                    if (mark1) {
                        display(Motion.OUT);
                    }
                    if (requestQueue.isSomeoneWantToIn(currentFloor, direction)
                            & (residualCapacity() != 0)) {
                        display(Motion.IN);
                    }
                    if (currentPeople.isEmpty()
                            & !requestQueue.isSomeoneWaitingAhead(currentFloor, direction)
                            & requestQueue.isSomeoneWantToTurn(currentFloor, direction)) {
                        elevatorTurn();
                        display(Motion.IN);
                    }
                    display(Motion.CLOSE);
                }
        
                boolean flag1 = !currentPeople.isEmpty();
                boolean flag2 = currentPeople.isEmpty()
                        & requestQueue.isSomeoneWaitingAhead(currentFloor, direction);
                boolean flag3 = currentPeople.isEmpty()
                        & requestQueue.isSomeoneWaitingBehind(currentFloor, direction);
                boolean flag4 = currentPeople.isEmpty()
                        & requestQueue.isEmpty() & (!processQueue.isRealEnd()) & (!reset);
                if (flag1 || flag2) {
                    elevatorMove();
                } else if (flag3) {
                    elevatorTurn();
                } else if (flag4) {
                    elevatorWait();
                }
            }
        }
        

        这样会导致出现很多意想不到的bug,电梯的运行逻辑没有得到适当的划分。因此在第三次作业中,增加了Controller类,对电梯的运行逻辑进行切分和控制。在这样的修改之后,Elevator的运行逻辑为:

        @Override
        public void run() {
            while (true) {
                switch (controller.getInstruction()) {
                    case OPNE_AND_CLOSE:
                        openAndClose();
                        break;
                    case MOVE:
                        move();
                        break;
                    case TURN:
                        turn();
                        break;
                    case WAIT:
                        if (requestQueue.size() != 0) {
                            break;
                        }
                        myWait();
                        break;
                    case RESET:
                        reset();
                        break;
                    case SUB_RESET:
                        subReset();
                        return;
                    case FINISH:
                        return;
                    default:
                }
            }
        }
        

        Controller一共可以做出7中行为决策,这些行为决策作为枚举类封装在了Instruction中:

        public enum Instruction {
            OPNE_AND_CLOSE,
            SPE_MOVE,
            MOVE,
            TURN,
            WAIT,
            RESET,
            SUB_RESET,
            FINISH
        }
        
    2. 画UML协作图(sequence diagram)来展示线程之间的协作关系

      img

    3. 识别出三次作业稳定的内容和易变的内容,并加以分析

    • 稳定的内容:

      1. ELevator类
      2. Elevator的等候队列RequestQueue
      3. ELevator的梯内队列CurrentPeople
        因为电梯本身的运行逻辑,以及对于分配到的等候队列、电梯内的请求队列的管理在三次作业中都没有改变。对于处理等候队列中的请求,三次作业都采取了Look策略,结合电梯当前的运行方向和楼层以及等候队列中的需求情况,来控制电梯的前进和掉头。
        if (requestQueue.hasNormalReset()) { return Instruction.RESET; }
        else if (requestQueue.hasDoubleCarReset()) { return Instruction.SUB_RESET; }
        else if (currentPeople.isSomeoneWantToOut(currentFloor)
                || (requestQueue.isSomeoneWantToIn(currentFloor, direction)
                & (elevator.getResidualCapacity() != 0))
                || (currentPeople.isEmpty()
                & !requestQueue.isSomeoneWaitingAhead(currentFloor, direction)
                & requestQueue.isSomeoneWantToTurn(currentFloor, direction))) {
            return Instruction.OPNE_AND_CLOSE;
        }
        else if (!currentPeople.isEmpty()
                || (currentPeople.isEmpty()
                & requestQueue.isSomeoneWaitingAhead(currentFloor, direction))) {
            return Instruction.MOVE;
        }
        else if (currentPeople.isEmpty()
                & requestQueue.isSomeoneWaitingBehind(currentFloor, direction)) {
            return Instruction.TURN;
        }
        else if (processQueue.isFinished()) {
            return Instruction.FINISH;
        }
        else {
            return Instruction.WAIT;
        }
        
    • 易变的内容:

      1. InputHandler输入处理器
        第二次作业新增了重置请求,第三次作业新增了双轿厢重置请求。因此都需要调整输入处理器。

      2. ELevator的run方法
        由于电梯的运行逻辑越来越复杂,在第二次作业中新增的重置请求使得电梯在运行前要先判断是否需要重置,并进行一些列操作:

        private void reset() {
            // 1. 清除现有人员
            if (!currentPeople.isEmpty()) {
                TimableOutput.println("OPEN-" + currentFloor + "-" + id);
                goToSleep(openTimeConsuming + closeTimeConsuming);
                currentPeople.release(currentFloor, true);
                TimableOutput.println("CLOSE-" + currentFloor + "-" + id);
            }
            // 2. 开始重置
            // 1. 打印开始
            TimableOutput.println("RESET_BEGIN-" + id);
            goToSleep(100);
            requestQueue.releaseVers();
            // 2. 睡觉
            goToSleep(1100);
            // 4. 打印结束
            TimableOutput.println("RESET_END-" + id);
            // 3. 更新数据
            capacity = requestQueue.getResetCapacity();
            moveSpeed = requestQueue.getResetSpeed();
            ProcessQueue.shared().subCounterForReset();
            requestQueue.removeReset();
        }
        

        在第三次作业中需要判断重置的类型,对于双轿厢电梯进行不同的逻辑操作:

        private void subReset() {
            // 1. 重置
            if (!currentPeople.isEmpty()) {
                TimableOutput.println("OPEN-" + currentFloor + "-" + id);
                goToSleep(openTimeConsuming + closeTimeConsuming);
                currentPeople.release(currentFloor, true);
                TimableOutput.println("CLOSE-" + currentFloor + "-" + id);
            }
            TimableOutput.println("RESET_BEGIN-" + id);
            goToSleep(100);
            requestQueue.releaseVers();
            goToSleep(1100);
            TimableOutput.println("RESET_END-" + id);
            // 2. 在调度器中删除此电梯,及其队列
            Scheduler.shared(null,null).removeEntry(this);
            // 3. 新建电梯: 锁共享,队列提前创建; 先启动电梯,再加入调度器
            RequestQueue requestQueueA = new RequestQueue(id,'A');
            RequestQueue requestQueueB = new RequestQueue(id,'B');
            SpecialMove specialMove = new SpecialMove();
            int resetCapacity = requestQueue.getResetCapacity();
            int resetMoveSpeed = requestQueue.getResetSpeed();
            int resetTransFloor = requestQueue.getResetTransFloor();
            SubElevator subElevatorA = new SubElevator(id,'A',specialMove,
                    resetCapacity,resetMoveSpeed,resetTransFloor,requestQueueA);
            subElevatorA.setName("Elevator " + id + "-A");
            SubElevator subElevatorB = new SubElevator(id,'B',specialMove,
                    resetCapacity,resetMoveSpeed,resetTransFloor,requestQueueB);
            subElevatorB.setName("Elevator " + id + "-B");
            ProcessQueue.shared().subCounterForReset();
            requestQueue.removeReset();
            subElevatorA.start();
            subElevatorB.start();
            Scheduler.shared(null,null).addEntry(requestQueueA,subElevatorA);
            Scheduler.shared(null,null).addEntry(requestQueueB,subElevatorB);
        }
        

三、分析自己在第三次作业中是如何实现双轿厢的两个轿厢不碰撞的

  • 对SpecialMove方法上锁
    设置了一个SpecialMove类,该类只含有一个move方法。该move方法包含了一系列的运行流程。
    包括了:

    1. 移动一层
    2. 开门
    3. 放人
    4. 关门
    5. 回退一层
  • 具体来说:

    1. 在创建A和B两个轿厢的时候,构造时传入同一个SpecialMove对象,使得SpecialMove成为两部电梯的共享对象。这样使得两部电梯在同一时刻,只会有其中一部电梯进行specialMove操作
      SpecialMove specialMove = new SpecialMove();
      SubElevator subElevatorA = new SubElevator(...,specialMove,...);
      SubElevator subElevatorB = new SubElevator(...,specialMove,...);
      
    2. SpecialMove包含了一下运行步骤:
      1. 移动一层
      2. 开门
      3. 放人
      4. 关门
      5. 回退一层
        public synchronized void move(SubElevator elevator) {
         elevator.move();
         elevator.openAndClose();
         if (elevator.myGetType() == 'A') {
             if (elevator.getDirection() == Direction.UP) {
                 elevator.turn();
             }
             elevator.move();
             notifyAll();
             return;
         } else {
             if (elevator.getDirection() == Direction.DOWN) {
                 elevator.turn();
             }
             elevator.move();
             notifyAll();
         }
        }
        
        这样就可以保证在即将移动到换乘楼层时,只有一部电梯占用SpecialMove进行运行,另一部电梯如果此时也要进入换乘楼层则必须等待,直到另一部电梯释放SpecialMove的锁

四、分析自己程序出现过的bug以及自己面对多线程程序的debug方法

  • InputHandler轮询
    • 在第二次作业中,使用InputHandler来控制所有线程的结束。这就导致在输入结束之后,InputHandler线程不会直接结束,而是反复查询请求完成情况,直到完成所有请求。这就造成了InputHandler轮询的情况,也就是一直在反复查询各个电梯的等候队列和梯内队列的情况。
    • debug的方式非常朴素,就是用System.out.println()输出,然后观察输出情况来定位是哪个线程造成了轮询
  • Scheduler轮询
    • 在第二次作业中,由于使用的调度机制是查看当前是否有合适的电梯分配请求,如果没有的话就回退请求到processQueue中,这就造成了,如果此时有大量需求涌入,但是所有电梯都在重置中,或者所有电梯的已经满员,那么就会回退大量请求,但是等Scheduler分配的时间片之后又会重新获取请求。因此造成了获取-回退-获取-回退的轮询问题。
    • 同样通过System.out.println()来debug,发现Scheduler发生了轮询

五、心得体会。从线程安全和层次化设计两个方面来梳理自己在本单元三次作业中获得的心得体会

  • 线程安全
    • 加锁机制:选择合适的锁来保护关键的共享资源,如主队列和电梯的请求队列。通过使用锁,可以确保在同一时间只有一个线程可以对这些资源进行操作,避免竞争条件的发生。
    • 同步块的使用:将对共享资源的操作封装在同步块中,以确保原子性操作。这样可以避免多个线程同时访问共享资源导致的数据不一致性。
    • 共享资源的访问控制:通过合理的设计和划分共享资源的访问权限,限制对共享资源的访问。例如,只允许特定线程访问主队列,以确保只有输入处理线程和调度器可以修改主队列。还有在限制换乘楼层的进入时,也实现了对于SpecialMove对象的互斥访问,使得只有一部电梯可以进入换乘楼层。
  • 层次化设计
    • 单例模式的应用:使用单例模式来管理主队列,确保在整个程序中只有一个主队列实例。这样可以方便地在不同模块间共享主队列,同时避免了对主队列的多重实例化和状态不一致的问题。同时Scheduler和InputHandler也采用了单例模式,这样可以在ELevator中方便的获取到Scheduler中的一些方法,比如重置成双轿厢电梯后往elevators中增加新的电梯。
    • 模块划分:将程序划分为输入处理模块、调度器模块和电梯线程模块等不同的功能模块,每个模块负责特定的任务。这样可以使程序结构清晰,易于理解和维护。
...全文
51 回复 打赏 收藏 转发到动态 举报
写回复
用AI写文章
回复
切换为时间正序
请发表友善的回复…
发表回复

301

社区成员

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

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