[BUAA OO]第二单元总结

[BUAA OO]第二单元总结

序.写在前面

​ 本单元我们要面对的任务是以电梯为载体的多线程问题,要求我们掌握多个线程的调配、线程之间的交互、cpu时间的合理分配,具体的作业要求为建立一个多线程电梯运行系统,实现对乘客的接送运载,具体的数据限制如下:

​ ● 第一次作业,A、B、C、D、E座各有一纵向电梯,只有纵向请求并且不能增加电梯

​ ● 第二次作业,电梯类型变为纵向电梯和横向电梯,请求有乘客请求和加电梯请求,其中乘客请求为纵向请求或横向请求,加电梯请求可以在任意座、任意层增加电梯

​ ● 第三次作业,较之第二次作业的新要求有:电梯的容量、速度、可达楼座可定制,乘客请求不设限制

一.架构设计

1.第一次作业

1.1调度器Schedule

读入请求:通过与InputThread共用一个waitQueue来获得InputThread读入的请求

处理请求:判断乘客请求是哪一座电梯的并分配至对于楼座

结束请求:在请求处理结束之后,会由响应线程直接丢弃,由于请求简单,所以没有线程向调度器的信息

结束线程:进行两步判断:是否已处理完现有请求,是否无新请求进入。

​ 若waitQueue为空,说明调度器已经处理完现有请求;若读取到null,说明无新请求进入。这样,schedule就会发出end信号告知各线程:已经不会有新请求进入了,如果你已经处理完现有请求,那么你就可以结束了。

1.2架构的变化&扩展

UML类图

变化

​ 本次多线程的信息交互架构继承了exp3:采用RequestQueue实现请求的交互,利用getOneRequest方法避免轮询。

    public synchronized PersonRequest getOneRequest() {
        // may not be the code now , should think more
        if (isEmpty() && !isEnd) {
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        if (personQueue.isEmpty()) {
            return null;
        }
        return personQueue.get(0);
    }

​ 其中getOneRequest成功帮助对于多线程还不是很了解的我避免了轮询,对我第一次作业的完成起到很大的帮助。不过模仿exp3的代码也带来了一些问题:由于没有吃透多线程的概念,所以我必须使用RequestQueue这个数据结构,但同时我又设计了以楼层为key的乘务表,这就导致在一定程度上的重复。并且由于look策略中不用使用RequestQueue,所以这也打消了我使用look策略的想法,最终在下一次进行了策略的更换。

​ 本次多线程的信息处理架构为查阅资料后使用的ALS策略:

    public synchronized PersonRequest setMainRequest() {
        if (myElevator.getInCrewWatch().numberInCrewWatch() == 0) {
            ......
            // may be null,remember to check it
        } else {
            int high = myElevator.getHighestFloor() - myElevator.getLowestFloor();
            int checkFloor;
            for (int i = 0;i <= high;i++) {
                checkFloor = myElevator.getCurrentFloor() + i;
                if ((myElevator.getLowestFloor() <= checkFloor && checkFloor <= myElevator.
                       ......
                }
                checkFloor = myElevator.getCurrentFloor() - i;
                if ((myElevator.getLowestFloor() <= checkFloor && checkFloor <= myElevator.getHighestFloor()) && 
                    myElevator.getInCrewWatch().judgePersonInFloor(checkFloor)) {
                    ......
                }
            }
            return null;
        }
    }

​ 该策略的核心点为设置主请求,设置主请求的思路见上代码,概括来说就是电梯内部有乘客,设置内部最早为主请求;内部无乘客,设置外部最早为主请求。之后就以主请求作为运动、暂停、捎带的判断基准运行电梯。

扩展

​ 虽然本次使用的ALS策略在之后的两次作业改换为了look策略,原因是在不考虑极端数据的情况下look策略在大部分数据上表现的更好,(并且look策略似乎较之ALS策略更加简洁)。不过关键的数据结构乘务表crewWatch和策略类为之后的扩展提供了方便,只需要进行少量修改就可以用于下一次的作业中(不得不说助教的提示真滴很有用

1.3线程之间的协作

2.第二次作业

2.1调度器Schedule

读入请求:从InputThread中读入请求(InputThread会对请求做预处理,判断请求是乘客请求还是电梯请求并且将请求传入对应的schedule中,传递必要参数)

处理请求:

​ 若请求是电梯请求,那么我们创建新电梯,将电梯存入schedule的电梯队列,并将电梯放入进程process,启动进程

​ 若进程是乘客请求,那么我们将乘客放入现载客量最少的电梯中

结束请求:

​ 请求结束后,由对应线程将请求丢弃

结束线程:

​ 当确定没有新请求时,schedule将传递End信号,告知电梯队列中所有电梯可以结束;等到相应进程的电梯中没有乘客并且已经收到结束信号时,该进程结束。

2.2线程的变化&扩展

变化:

​ 本次在第一次作业的基础上将策略改为抽象类,有纵向策略和横向策略两个子类,采用工厂模式新建策略对象;

    public synchronized void setDirection() {
        if (myElevator.getInCrewWatch().isEmpty() &&
                !myElevator.getOutCrewWatch().hasSameDirectionOut(
                        myElevator.getCurrentFloor(),myElevator.getDirection())) {
            myElevator.setDirection(-1 * myElevator.getDirection());
        }
    }
   public synchronized boolean hasSameDirectionOut(int currentFloor,int direction) {
        return currentFloorHasSameDir(currentFloor,direction)
                || otherFloorHasSameDir(currentFloor,direction);
    }
   public synchronized boolean currentFloorHasSameDir(int currentFloor,int direction) {
        boolean currentFloorHas = false;
        if (judgePersonInFloor(currentFloor)) {
            ......
        }
        return currentFloorHas;
    }
   public synchronized boolean otherFloorHasSameDir(int currentFloor,int direction) {
        boolean otherFloorHas = false;
        int i = currentFloor + direction;
        while (1 <= i && i <= 10) {
            ......
        }
        return otherFloorHas;
    }

​ 将ALS策略改换为look策略,与ALS策略以主请求为主不同,look策略以方向的确定为主,概括来说就是当电梯内部没有乘客&电梯外部同方向上没有同方向请求的乘客时,电梯更换方向。

​ 新增的横向电梯采用顺时针策略,就是保持顺时针旋转,遇到乘客且电梯未满就捎带,由于横向电梯快而且可以转圈,所以并不会损失太多性能。

扩展:

​ 本次的策略类、数据结构crewWatch、look策略在第三次作业中都在稍微修改后即可使用,同时因为在搭建电梯时将电梯、进程、调度器分开构建解耦,所以在第三次作业中更换调度器为Controller时只需改写schedule即可,为扩展带来了不小的便利。

2.3架构的变化&扩展

3.第三次作业

3.1调度器Controller&Counter

读入请求:从Main/电梯系统中读入请求

处理请求:首先,判断是初次请求还是二次请求;

​ 如果是初次请求,那么就判断请求是乘客请求还是加电梯请求;

​ 如果是加电梯请求,进一步分析:是横向电梯请求还是纵向电梯请求?应该属于哪个楼座(楼层)?,之后在相应楼座(楼层)加入电梯并将其放入线程process,启动新线程。

​ 如果是乘客请求,那么进一步分析:是横向请求、纵向请求还是横纵向请求?通过现有的电梯系统完成这个请求一共需要几步?把这些作为属性存入相应的乘客对象之后将乘客放入电梯系统。

​ 如果是二次请求,那么一定是乘客请求,查看乘客的工作阶段等属性,将乘客分配至完成其下一阶段工作的电梯中。

结束请求:由于我在本体中采用了流水线的调度模式,所以对于每一个乘客请求,在一次处理结束后要进行判断:是否已完成了所有阶段的工作?

​ 如果已经完成了所有阶段的工作,那么Counter计数器加1,表示有一个请求已经完成;

​ 如果没有完成所有阶段的工作,那么就让Controller再次读入请求,进行处理。

结束线程:结束线程时,我们要进行两步判断:是否已读入了所有的请求&是否已结束了所有的请求?

​ 第一步判断通过程序的设计实现:读到null推出读请求循环;

​ 第二步判断通过Counter实现:在第一步中,我们记录了乘客请求的数目personNum;在本步中,我们通过release将已经完成的请求释放,当释放的请求数目与personNum相同时,标志着所有请求已经结束,可以结束所有线程。

​ 结束线程时,我们将End置真,并唤醒各个线程查看,告知各个线程程序可以结束。

3.2架构的变化&扩展

UML类图

变化

​ 本次作业中由于乘客请求不再限制为单一方向,所以我采用了流水线的调度模式,这样一来,就需要更改之前的调度器schedule为Controller,将schedule作为二级调度器或者说调度器到电梯之间的一个过渡;并且进程的结束方式也要做相应的变换:由原来的调度器逐级告知end&本进程请求为空改为由调度器统一通知end&本进程为空,所以新增了统计是否处理完所有请求的Controller。

    public void addRequest(Request request) { //增添新的请求,并唤醒所有等待线程
        int index;
        if (request instanceof ElevatorRequest) {
            ElevatorRequest eleReq = (ElevatorRequest) request;
            index = elevatorBelong(eleReq);
            schedules.get(index).addElevator(eleReq.getElevatorId(),eleReq.getCapacity(),
                    eleReq.getSpeed(),eleReq.getSwitchInfo());
        } else if (request instanceof PersonRequest) {
            ParsedPerson parsedPerson = new ParsedPerson((PersonRequest) request);
            BitSet unfinishedStages = parsedPerson.getUnfinishedStages();
            if (!unfinishedStages.isEmpty()) {
                if (unfinishedStages.get(0)) {
                    parsedPerson.refineWorkingStage(0);
                    schedules.get(parsedPerson.getFirstBuilding() + 10).addPerson(parsedPerson);
                } else if (unfinishedStages.get(1)) {
                    parsedPerson.refineWorkingStage(1);
                    schedules.get(parsedPerson.getTransferFloor() - 1).addPerson(parsedPerson);
                } else if (unfinishedStages.get(2)) {
                    parsedPerson.refineWorkingStage(2);
                    schedules.get(parsedPerson.getSecondBuilding() + 10).addPerson(parsedPerson);
                }
            }
        }
    }
    public synchronized void release() { //代表完成一个任务
        count += 1;
        notifyAll();
    }

    public synchronized void acquire() { //检验一个任务的完成,如果没有已完成的任务,等待
        while (true) {
            if (count > 0) {
                count -= 1;
                break;
            } else {
                try {
                    wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

​ 此外,还更改了横向电梯的运行策略,由于本次横向电梯的速度、容量可定制,对结果的影响变大,所以我决定不再使用顺时针策略,而使用一种半look策略:在look策略的基础上对最劣情况进行特判。

​ 值得一提的是,在电梯的分配上,由于本次不同电梯的容量、速度都可能不同,所以采取将人数最少的电梯分配出去的策略已经不再合理,我将电梯人数 / (容量 * 速度)作为新的判断因子进行电梯的分配。

扩展

​ 本次作业依旧采用了抽象策略类,可以适应进一步扩展;如果还有更加复杂的电梯,可以考虑将ALS/look作为策略的策略,进行纵向/横向策略和ALS/look的组合。

3.3线程之间的协作

二.同步块&锁

1.第一次作业

​ 第一次作业的同步块与锁基本上参考exp3,由于之前没有构建过多线程系统,所以在所有涉及多线程的地方都加上了synchronized,并且还使用了CopyOnWriteArrayList和ConcurrentHashMap,不过之后再观察代码发现并没有这个必要,只需要对更改RequestQueue的方法和getOneRequest上锁就可以了。

 public synchronized void remove(PersonRequest request) {
        personQueue.remove(request);
        notifyAll();
    }
 public synchronized void addRequest(PersonRequest request) {
        personQueue.add(request);
        notifyAll();
    }
public synchronized PersonRequest getOneRequest() {
        // may not be the code now , should think more
        if (isEmpty() && !isEnd) {
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        if (personQueue.isEmpty()) {
            return null;
        }
        return personQueue.get(0);
    }

​ 对于更改方法上锁很常见,值得一提的是对get方法上锁,这样是为了在空时锁住进程,防止轮询。

2.第二次作业

​ 本次作业该换了look策略,不再使用requestQueue数据结构,所以本次作业的同步机制是我自己在实验的基础上写的。

 	public synchronized void addPortraitInRequest(PersonRequest person) {
        int keyFloor = person.getToFloor();
        if (crewWatch.containsKey(keyFloor)) {
            crewWatch.get(keyFloor).add(person);
        } else {
            CopyOnWriteArrayList<PersonRequest> floorReq = new CopyOnWriteArrayList<>();
            floorReq.add(person);
            crewWatch.put(keyFloor,floorReq);
        }
        notifyAll();
    }

    public synchronized void addTransverseOutRequest(PersonRequest person) {
        int keyFloor = person.getFromBuilding() - 'A' + 1;
        if (crewWatch.containsKey(keyFloor)) {
            crewWatch.get(keyFloor).add(person);
        } else {
            CopyOnWriteArrayList<PersonRequest> floorReq = new CopyOnWriteArrayList<>();
            floorReq.add(person);
            crewWatch.put(keyFloor,floorReq);
        }
        notifyAll();
    }
	public void exeOnce() {
        if (!isEnd && isEmpty()) {
            outCrewWatch.await();
        }
        myStrategy.exeOnce();
    }
    public synchronized void await() {
        try {
            wait();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

​ 主要是对CrewWatch的增加、删改方法使用synchronized上锁,同时使用方法await来避免轮询。主要上锁的对象时CrewWatch,增加、删改方法的上锁是为了保证同步性;await则是通过对CrewWatch上锁锁住这个进程,防止轮询。

3.第三次作业

​ 本次作业的同步块在第二次作业的基础上(对CrewWatch的增加、删改方法上锁),新增了对RequestCounter的release和acquire方法上锁,并且在每个schedule中增加了一个唤醒方法用来在最后唤醒schedule中wait进程,开始依次结束进程。同时在使用await避免轮询的条件有一些变化。

​ 其中RequestCounter的两个方法上锁是为了防止有进程进行release的同时Main进行acquire,schedule的唤醒方法则是因为如果schedule中所有电梯进程都已经处理完自己的请求并进入休眠,那么在Countroller传出end信号时没有进程可以接受到,从而使得所有进程无法结束。因此必须由外力进行唤醒。

    public synchronized void release() { //代表完成一个任务
        count += 1;
        notifyAll();
    }

    public synchronized void acquire() { //检验一个任务的完成,如果没有已完成的任务,等待
        while (true) {
            if (count > 0) {
                count -= 1;
                break;
            } else {
                try {
                    wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    public void notifyAllElevators() {
        for (CrewWatch crewWatch : crewWatches) {
            synchronized (crewWatch) {
                crewWatch.notifyAll();
            }
        }
    }
    public void exeOnce() {
        if (!Controller.getInstance().getEndTag() && isEmpty()) {
            outCrewWatch.await();
        }
        myStrategy.exeOnce();
    }

三.bug分析

1.自身bug

​ 本次在强测中未被发现bug,在第三次作业互测中被发现了一个bug,这个bug是因为我没有判断横向请求所在楼层是否相应的横向电梯,当时在互测发现自己被hack之后就用群里同学的数据试了试,就发现了这个bug,当时吓得够呛,因为这个bug还是比较好触发的,狠狠把自己批斗了一番。

​ 会有这个bug的原因是在写程序的时候考虑不周,自己在测试数据时也只关注了比较复杂的请求叠加的处理,测试基本的横向电梯时偷懒了,使用的第六次作业的数据,所以就漏掉了这种情况。最后又因为懒没有通读自己的代码......不幸中的万幸是强测没有这个点,下次一定注意,从基本数据出发,并且要通读代码、理清自己的逻辑。

2.他人bug

​ 第一次作业中我把用来测试自己的手搓数据交上去hack中了两个点,一个点是优化时出了点问题,时间比限制时间还短,一个是输出顺序不对,应该是没有保护输出。

​ 第二次一刀没中......

​ 第三次也一刀没中,在本地测试时hack到了一个点,但是交上去就没法复现了。

​ 时间实在是有点紧,所以我没认真看每个同学的代码,只能把测试自己的数据上交,效果不理想也是在意料之中的。

四.心得体会

在学习指导书内容之后再写代码

​ 本单元的体验较之上一单元要好得多,原因就是在本单元,笔者先研读了实验课的代码之后再完成作业,使用的是“主流方法”;反观上一单元,因为按照自己的思路,没有认真学习使用“递归下降”的思路,导致代码的难度和bug的数量直线上升。这告诉我们:一定、一定、一定按照课程的思路来完成作业。

多线程

​ 本单元是笔者第一次接触多线程,从刚开始的啥都不懂、惴惴不安,恨不得给每个代码块都加一个synchronized,到后来实现了一定程度上的"精确打击":在需要的地方上锁,在获得满满成就感的同时也积累了不少经验。

​ 首先是对于多线程的问题,一定要先设计好整个问题的结构在动手,因为多线程的bug并不好找,暴力输入也很难起到较好的结果,而且多线程的同步块设置在什么地方既简洁又能起到上锁的作用也需要提前考虑清楚,所以在开始之前一定要做好充足的准备;其次,就是要会利用工具IDEA,可以通过设置线程断点、观察不同线程的运行情况(一个照相机状的按钮)来判断自己程序的bug在哪;最后是要在写完之后通读自己的代码,看看自己的代码有没有实现自己想要实现的逻辑。

层次化设计

​ 本单元笔者真实地体会到了层次化设计带来的好处,笔者从第一次作业就注意功能的解耦、将模块独立,果然为之后的作业带来了不小的便利,许多数据结构只需要进行一些优化就可以适应新的需求,更改一个模块的功能也不会对其他模块造成影响。

写在最后

​ 从无到有地实现了多线程不得不说是一件很有成就感的事,同时先构思再写代码的思路也让我受益匪浅,当然也有没有实现自动评测、电梯分配简单的遗憾,收拾心情,我们下周再见ヾ(o・ω・)ノ

posted @ 2022-05-03 17:10  Jack_rbkd  阅读(28)  评论(0编辑  收藏  举报