北航OO第二单元总结

第二单元我们在第一单元单一进程面向对象的设计基础之上,进一步学习和练习使用了多进程。目标是模拟多线程实时电梯系统,熟悉线程的创建、运行等基本操作。

一、同步块的设置和锁的选择

本单元主要用到“生产者-消费者”模型。在作业中,对控制器中的主从请求的添加与获取采用了 synchronized 关键字加锁。

通过设置flg控制开始与结束,电梯调度算法方面,采用了look算法,并进行了一定的优化,通过使用wait()和notifyAll(),电梯在空闲时不会无脑疯狂调转方向,从而更贴近真实情况。Look算法据我的理解是一种分治算法,具体说来先判断电梯有无人:如果有人判断是否需要开门,不需要则继续沿原方向行进;如果没有人则进一步判断有没有请求,若没有请求则原地等待,若有请求则按原方向行进。

电梯对同步块的访问基本都是修改型的,例如“添加 request”,“获取 request”,如果不加锁,可能就会造成资源的共享导致错误(例如一个人同时进了两个电梯)。

public void run() {
        String pattern = elevatorInput.getArrivingPattern();
        scheduler.setPattern(pattern);
        PersonRequest personRequest;
        Request request;
        MyRequest myRequest;
        while ((request = elevatorInput.nextRequest()) != null) {
            String reqStr = request.toString();
            int index = reqStr.indexOf("ADD-");
            if (index == 0) {
                int lastIndex = reqStr.lastIndexOf("-");
                String id = reqStr.substring(4, lastIndex);
                //System.out.println(id);
                String type = reqStr.substring(lastIndex + 1);
                //System.out.println(type);
                //System.out.println("新增电梯-"+id+"-"+type);
                Elevator elevator;
                // 增加新的电梯
                switch (type) {
                    case "A":
                        elevator = new Elevator(id, 8, 600, floorA, scheduler);
                        elevator.start();
                        break;
                    case "B":
                        elevator = new Elevator(id, 8, 600, floorB, scheduler);
                        elevator.start();
                        break;
                    case "C":
                        elevator = new Elevator(id, 8, 600, floorC, scheduler);
                        elevator.start();
                        break;
                    default:
                        break;
                }
                continue;
            }
            personRequest = (PersonRequest) request;
            //System.out.println(personRequest);
            myRequest = new MyRequest(personRequest.getFromFloor(),
                    personRequest.getToFloor(),
                    personRequest.getPersonId());
            scheduler.putRequest(myRequest);
        }
        scheduler.setFinished();

        try {
            elevatorInput.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

二、调度器设计

调度器负责接受从输入线程传来的请求,将其放入请求列表。电梯通过暴力循环请求列表竞争任务,获取或者完成任务后更新请求列表。第二次作业开始调度器单独作为一个线程,根据我的设计,调度器与输入线程共享总等待队列的对象,调度器与每个电梯共享电梯自己的等待队列的对象,即调度器是输入线程和电梯线程之间的桥梁,从输入线程接收到乘客的需求,然后分配到各个电梯进行处理,这样看来,电梯就只需要根据自己的等待队列进行运行,调度器的任务就是根据总等待队列和每个电梯的等待队列综合分配乘客到各个电梯,具体的调度方法也很简单很暴力:算出所有电梯的等待队列的人数和总等待队列的人数和,进而算出平均人数,然后把总等待队列中的乘客分配到每个电梯中使得每个电梯中的等待队列的人数达到平均人数。

//  检查移动方向
private void checkDirection() {
    if (currentPersonNum == 0 && !scheduler.isInRightDirection(
            floor, currentFloor, currentDirection)) {
        currentDirection = -currentDirection;
        // System.out.println(String.format(
        // "%s: Direction turned at floor %d, now go %s",
        // elevatorId, currentFloor,
        // (currentDirection == 1) ? "up" : "down"));
    }
}

三、历次作业回顾

1.第五次作业

​ 本次作业中,每个楼座都由一个对应的RequestTable。每个RequestTable中,按照出发楼层将需求分别存放在10个队列中,这10个队列则由一个HashMap存储,其中Key是楼层,Value是队列。

​我的调度策略与非常流行的Look策略大致相同,在强测中也取得了不错的性能表现,LOOK算法更像是就近远端捎带,在将电梯内部乘客全部送达后形成空梯时会沿此方向去处理远端请求,避免了底层和高层请求因为难于捎带而堆积后的空转问题,效率较好。具体如下:

目的楼层的确定

  • 若电梯内有乘客:以电梯内乘客当前方向上的最远楼层为目的楼层
  • 若电梯内没有乘客:
    • 若当前方向上的楼层有等待中乘客:以当前方向上最远的乘客所在楼层为目标楼层
    • 若当前方向上的楼层没有等待中乘客:转向,并按照上述方式确定目标楼层

线程交互

​ 采用生产者-消费者模式,以Input为生产者、Elevator为消费者,二者通过共享对象RequestTable进行交互。

UML类图:

 

 

时序图:

 

 

 

2.第六次作业

在第二次作业中电梯系统增加了增加电梯和横向运输乘客的功能。其中增加电梯的请求我在Input类里实现,当接收到该类型的请求时我便新开一个电梯线程,而其中会出现一个楼座会有两个及以上的电梯,因此我在同座或同层电梯之间采用自由竞争的策略,多个电梯共享一个processingQueue,每个电梯去寻找适合自己处理的请求,谁最先抢到谁就先处理。

横向电梯的调度策略不同于纵向电梯,因为横向电梯是环形电梯,周而复始,没有尽头,若采用LOOK算法会出现难以掉头的情况,因而我采用ALS算法,不考虑待处理队列中的请求,仅考虑将电梯内部的请求作为主请求,提高电梯的掉头频率。

 UML类图:

 

 

 

 

时序图:

 

 

 

3.第七次作业

本次作业主要有以下两个不同点:

  • 扩大乘客移动需求:即乘客的出发楼座和出发楼层、目的楼座和目的楼层均可以不同,这就使得乘客一定需要换乘;
  • 电梯定制化:可以定制电梯的运行速度、可容纳人数、是否能在某座开关门(横向电梯可设置)

具体来说,初始时在五座分别有一个纵向电梯,同时还有一个在1层的可达所有楼座的横向电梯。在程序运行中间可以增加定制化的电梯。

我们在分配时,应该权衡距离与等待运输压力来对换乘电梯进行选择。

首先,我们在楼栋/楼层分配器中对路径进行规划。先找到所有可达路径中,移动路程最小的,如果移动路程最小的路径有多个,我们就从中选出横向楼层压力最小的。

为此我们需要给出一个运输压力评估方法,取整个电梯等待任务队列中所有的乘客数为电梯的压力数。每当电梯中加入一个人时,其压力数自增,而一个人离开电梯时,该电梯压力数自减。楼层或楼栋的压力数即为其下属所有电梯的压力数之和。

之后直接将划分好的任务序列投放至对应的楼栋/楼层,又楼层来对各自负责的部分进行电梯分配,分配完成后将任务向电梯投放。与前两次作业不同的是,这次投放的位置不再是等待队列。而是电梯类中新增的一个notAvailable队列,代表还未生效的请求。每次任务完成一个阶段时,会对下一阶段的电梯进行notifyAll()操作,唤醒电梯将其加入等待队列。

UML类图:

 

 

时序图:

 

 

 

四、分析自己程序的bug

第一次作业被hack出一个RTLE的bug,原因是因为使用了暴力轮询来使电梯获取调度器请求队列内的请求。后来在请求队列没有请求的时候 wait() 一下就好了。

第二次作业被测出一个bug,我在规划路线和判断是否有请求的时候都判断了横向电梯的可停靠问题,可是在电梯从队列里接人的时候忘记判断,导致有可能有人上了他不该上的电梯,导致横向电梯一直循环,最后导致了rtle。

五、互测策略

1.有效性

本单元并未使用严密的科学测试策略,仅凭借自己debug过程中碰到的易错点与交流群中讨论的易错点,手动构造测试样例进行测试,严谨度与精准度不高。

2.线程安全

线程安全主要先浏览代码,找到存在线程共用的变量或类型,构造能够使共用变量出现同时访问或频繁变化的请求队列。

3.与第一单元的差异

第一单元由于表达式解析的流程较为固定,不同同学的代码出现共同逻辑漏洞的可能性较高,某些特殊表达的的通用性较强,互测找出bug较为容易。

第二单元对于电梯的运行策略,具体实现方式的灵活性较高,难以找到某个共通的bug,需要对对方的代码进行一定程度解读以定位可能的bug;除此之外,由于多线程的特性,某些bug可能难以复现,幸运的是,在每次互测结束时,课程团队会对互测样例进行进一步的测试以复现bug。

六、感想

从知识点上来看,这是一次对“生产者-消费者”模式的再回溯。本单元的三次作业我都采用了半重构的方法进行迭代开发。对于代码变化较小的部分(如纵向电梯),我也是对着上一次作业的代码重打了一次,导致三次作业迭代开发所用的时间都挺长的。虽然这样可以更好的保证正确性,但其实没必要,完全可以利用IDEA来定位代码中需要修改的部分,减小开发的工作量,这启示我以后写面向对象的代码还是要注重层次性,以免事倍功半。

 

posted @ 2022-05-04 03:08  Charlie_Cosmos  阅读(98)  评论(0编辑  收藏  举报