BUAA OO 第二单元

吴瑜-22230605 学生 2024-04-16 22:05:14

要点快速查看

  • 三次作业中同步块的设置和锁的选择(见0.2)

  • 调度器设计

    • hw5 UML图及架构分析(见1.1)
    • hw6 UML图及架构分析(见2.1)
    • hw7 UML图及架构分析(见3.1)
  • 调度策略优化

    • hw5 优点(见1.3)
    • hw6 优点(见2.3)
    • hw7 优点(见3.3)
  • 实现双轿厢的两个轿厢不碰撞(见3.2)

  • bug与debug(见4,5)

  • 心得体会(见6)

0. 主要知识点

本单元主要考查了多线程和锁。

  1. 多线程方面,主要涉及开启.start()运行.run()结束.run()中跳出while(true)循环(break/return)。这一点看课程组给的demo样例很容易能学会。

  2. 锁方面,其实前两次作业我对锁的概念和使用都囫囵吞枣,直到hw6强测wa了两个点。我主要使用了synchronized这种锁,它有以下三种使用方式:

    • 修饰静态方法

    • 修饰实例方法

    • 修饰代码块

    由于不同线程之间,只需要对共享对象的写入或移除上锁,所以我采用了修饰代码块这种方式,以下是一个例子。

    synchronized (globalRequest) {
        while (globalRequest.getSize() > 0) {
            processPerson(globalRequest.getRequest(0));
            globalRequest.removeRequest(globalRequest.getRequest(0));
        }
        globalRequest.notifyAll();
    }
    

1. homework_5

1.1 UML图及架构分析

UML图

img

线程关系

img

架构分析

Main类:创建含六部电梯的Arraylist<Elevator> elevators,开启Scheduler和六个Elevator进程。

RequestPool类:请求的列表集合。

Scheduler类:主要承担了读取请求、向对应电梯加入请求的功能,由于本次作业指定了每个请求对应的电梯、不需要自己写分配策略,所以结合后续作业来看,这里的Scheduler命名为Input更合理一些。

Elevator类:主要承担了电梯的运行。请求存放方面,每部电梯创建了inRequestoutRequest两个请求池来存放该部电梯电梯里外的请求,每当一个请求进入电梯,则由oueRequest转移至inRequest;电梯运行方面,主要借鉴了BUAA-OO-第二单元:电梯调度 | YannaのBlog (gitee.io)的思路,以下是一个简略的思维导图:

img

1.2 代码量与复杂度分析

代码量

img

类复杂度

img

Elevator类中包括了大量的运行判断:

  • 寻找主要请求searchMain()open()
  • 上行还是下行run()
  • 是否开门judgeOpen()
  • 是否进人open2()open4()
  • 是否出人open1()

而无论是寻找主要请求、是否开门,还是进人、出人,几乎都要遍历inRequestoutRequest,因此复杂度较高。

1.3 优缺点

优点

  1. 在寻找主要请求时,使用了 ALS 策略和look算法(look算法参考BUAA-OO-第二单元:电梯调度 | YannaのBlog (gitee.io)),尽可能的捎带,性能良好。
  2. 利用look算法做到尽可能不掉头,以减少耗电。
  3. 在上锁方面,对每部电梯的outRequest的添加和移除上锁,上锁严谨且完全,没有出现死锁、上锁错误、缺锁导致的bug。

缺点

  1. Scheduler类承担了读取请求、分配请求两重功能,结构不清晰,不利于后续迭代。
  2. 讨论中发现有的同学将电梯运行时的判断和电梯线程分开,单独成类;而我是将其放在电梯线程中,随着迭代增加,hw7出现类的行数超过500行、方法行超过60行的现象,最终不得不将其拆分,如果第一次作业能"高瞻远瞩"、尽可能解耦,就可以避免这样尴尬的局面。
  3. 判断开门的方法写的太冗杂,其实自己也大概知道有些情况之前已经判断过了,但还是为了图省事再判断了一遍。(这也是hw7方法行、类行爆掉的一个主要原因吧)(扶额苦笑无奈

2. homework_6

2.1 UML图及架构分析

UML图

img

线程关系

img

将上次作业中的Scheduler类解耦成Input类和Scheduler类:

  • Input类:

    • 负责读取请求:如果为 PersonRequest 则加入 globalRequest;如果是ResetRequest,则通知对应电梯处理。
    • 计数:count记录已处理完毕的请求数目,size记录读取到的请求总数。
  • Scheduler类:负责从 globalRequest 读取请求并分配,通知对应电梯处理请求。

    分配策略方面我选择了 满足捎带>出发地再电梯运行方向上(优先最近)>电梯现有请求数最少 三种分配方式,永远存在电梯请求数最少的电梯,故请求总能被分配。

Elevator类中新增了对reset的处理:

  1. 若电梯请求为空,则立即开始reset处理;若电梯内请求不为空,则最多输出两个arrive,开始reset处理。
  2. reset开始前,将电梯内外的请求丢回 globalRequest。
  3. 电梯内新建变量requestTemp:reset过程中仍可向该电梯加入请求,这些请求被暂存在requestTemp中,在reset结束后被移入outRequest。

2.2 代码量与复杂度分析

代码量

img

类复杂度

img

Elevator

  • open系列同上次作业
  • reset时清除电梯内外请求cleanIn()cleanOut():需要遍历inRequestoutRequest,将请求移至 globalRequest,复杂度较高

Scheduler类主要包括四种分配策略(way3()弃用):

  • 寻找满足捎带的电梯(优先最近)way1()
  • 寻找出发地在电梯运行方向上的电梯(优先最近)way2()
  • 寻找现有请求数最少的电梯 (优先最近)way4()

每种分配都需要遍历各部电梯、获取每部电梯的详细信息,复杂度较高。

2.3 优缺点

优点

  1. 若电梯内请求不为空,则最多输出两个arrive,开始reset处理:这种方式能使电梯内的请求离toFloor更进一步,优化性能。(考虑过比较 该请求目前所在的电梯的移动速度 和 重新分配后所在的电梯的移动速度,但由于清明节想出去玩没有实现)

  2. 起初我没有给正在reset的电梯分配请求,在舍友的帮助下,注意到 “全部电梯一起reset、reset_end之后所有请求都分配给第一部reset_end的电梯” 的情况,于是想到了暂存的方法(reset过程中仍可向该电梯加入请求,这些请求被暂存在requestTemp中,在reset结束后被移入outRequest)。

  3. (同样是在舍友的帮助下)我学习了量子电梯,虽然在时间上与更合理的分配相比微乎其微,但扩展了思路。

    openPoint = System.currentTimeMillis();
    // 开门操作(进人、出人)
    closePoint = System.currentTimeMillis();
    try {
        sleep(max(openTime + closeTime - (closePoint - openPoint), 0));
    } catch (InterruptedException e) {
        throw new RuntimeException(e);
    }
    

缺点

  1. 上锁不全面:Scheduler类从 globalRequest 读取请求后,删掉请求时,没有对其上锁,导致强测互测中出现了如下的bug:

img


(第二个RECEIVE-35其实应该是RECEIVE-36)

  1. 由于时间原因、以及害怕重构过后原有的正确性无法保证,并没有对冗杂的open系列方法进行重构。

3. homework_7

3.1 UML图及架构分析

UML图

img

线程关系

img

Input类同上。

Scheduler类:分配策略基本同上。只添加了:出发层在电梯运行范围内。(这里出现了一个巨大的bug,导致部分情况性能很差)

将上次作业中的Elevator类分为三个类:

  • Elevator类:新增了type(表示电梯类型:null/A/B),transferFloor(换乘楼层),lowFloor/highFloor(电梯的最低层和最高层),使得两类电梯使用同一套运行逻辑;新增requestPoolArequestPoolB,使得电梯在进行DoubleReset时,也能从外界读入请求,分别暂存在以上两个容器中,并在A/B电梯线程新建时,读入到对应电梯中的outRequest中。

  • Operation类:将电梯运行中冗杂的判断开门/寻找主要请求操作挪到了该类中。

  • Input类:输出语句类

3.2 代码量与复杂度分析

代码量

img

类复杂度

img

Operation类包含了电梯运行过程中大量冗杂的判断函数(open()searchMain()),复杂度较高。

Scheduler类主要包括三种分配策略way()以及对应的优先到达层在输出范围内的分配策略mainWay(),每种分配都需要遍历各部电梯、获取每部电梯的详细信息,复杂度较高。

Elevator类中,当对应的“朋友电梯”在换乘层时,该电梯不能达到换乘层,因此在up()down()方法中包含了本电梯的wait和对方电梯的notifyall:实现双轿厢的两个轿厢不碰撞

public void Up() {
    int lastFloor = nowFloor;
    
    if 不是换乘电梯:上行(nowFloor++)
    else if 是A电梯:
            if 将要行至换乘层:
                if 换乘层未被占用且暂时不会被占用:上行(nowFloor++)
                else  transferFloor.wait();
            else:上行
    else:上行(nowFloor++)
    
    if (是换乘电梯 且 lastFloor是换乘层) { // 该电梯从换乘层离开
        synchronized (friend.transferFloor) {
            friend.transferFloor.notifyAll();
        }
    }
}

3.3 优缺点

优点

  1. 没有对A/B型电梯新建一个类单独处理,而是通过标识type做区分。
  2. 将print输出单独成类,将运行与输出解耦,结构清晰。
  3. 双轿电梯优先,以节省电量。

缺点

  1. 对于新的开门条件openA()openB(),其实与之前原有的判断条件有重合,但我没有做详细梳理,而是整个一坨加在了原先开门条件的前面,代码逻辑冗杂。
  2. 由于前两次均没有重构,逻辑冗杂,这次作业直接导致Elevator类远远超出了500行限制,不得不新建一个Operation类、将判断操作挪出来。
  3. 上次作业的强测wa了两个点,反思发现在过度追求性能时反复修改代码,导致错误,故这次作业有些畏手畏脚,想多做测试保证正确性,对A/B型电梯没有做什么性能优化。
  4. 接上条,由于没有关注性能、测试不完全,导致对于部分电梯分裂的情况,请求会涌入正常的电梯,运行时间过长,产生TLE。

4. 出现的bug

在hw6强测和互测中,暴露出一个bug,即上锁不完全——Scheduler类从 globalRequest 读取请求后,删掉请求时,没有对其上锁。导致某些时候分配请求错乱、输出错乱。

在hw7强测中,部分电梯分裂的情况,请求会涌入正常的电梯,运行时间过长,产生TLE。(写的时候脑子不清楚写反了,考虑到耗电,应该优先考虑双轿电梯,而且我实际的实现是先考虑正常电梯,并且由于有一个寻找最少请求的方法,导致只有一部正常电梯时所有请求都涌入该部电梯)。

在交作业前、自己测试中那可就出现了不少问题

  • 加锁不完全:导致某个请求仿佛加进去了,但实际上没有;电梯好像从电梯容器中删掉了,但实际上没有。
  • 结束进程的条件不正确:比如hw6六部电梯最后reset无法结束。
  • TLE:起初我不对正在reset的电梯分配请求,所有请求被分配给第一个reset结束的电梯,导致允许时间过长。
  • CPUTLE:hw6一开始我没有新建Input线程,Scheduler同时承担读入和分配两个功能,会导致轮询。
  • receive输出:起初我是先addRequest再输出receive,很可能导致该请求的in输出前于recive输出。
  • A/B电梯相撞:此时判断换乘层空,而两部电梯下一步都想到达换乘层,两部电梯会相撞。
  • 空开门:判断开门的条件和实际开门人的进出不吻合(其实这一点我从hw5开始就是错的,但hw5和hw6强测互测都没被发现,直到在hw7自己测试一组数据时发现一开始就空开门了(紧张刺激))

在写博客的时候发现了hw7的一个bug,在分配策略中,笔误把一个getReset()写成了getDirection(),还好问题不大,,因为最后请求总能分出去的……

5. debug方法

  1. 对于较短的数据,脑子里过一遍逻辑。
  2. print:将关键时刻输出,看有没有进入、此时的主要请求是谁。当然记得提交的时候把它删掉:)
  3. arrive输出语句注释掉(arrive过多不易观察请求的进出)。当然记得提交的时候把它放出来:)
  4. word查找:个人觉得这个方法特别好用,对于很长的数据,利用word的查找功能能快速跟踪某个请求id或者某部电梯的状态。
  5. 线程安全:即可恶的概率问题,难以复现,debug之后也不知道自己改对了没有。对此,我写了一个简易的测评机:对同一组输入运行上百遍,标记运行时间超出正常运行时间的输出。成百上千次跑下来没有TLE、输出正确,基本上能说明没有问题。
  6. 手动构造数据检查:
    • 单独一条reset,看是否能开启reset、是否能结束所有进程
    • TLE:50s时输入70条数据,并reset为最慢速度和最少容量
    • TLE:六部电梯reset时,输入大量请求
    • 大量reset进入/数据开头reset全部电梯/数据最后reset全部电梯

6. 心得体会

6.1 线程安全

  1. 同步方法和代码块:

    Up()Down() 方法中使用了 synchronized 块来同步访问 transferLock 对象。

    ElevatorInputScheduler之间使用 synchronized 块来同步访问 outRequestglobalRequestelevators 对象,因为这些对象涉及到多线程并发访问共享,因此上锁以确保线程安全。

  2. 线程间通信: 使用了 wait()notifyAll() 方法来进行线程间的等待和唤醒,二者配对使用,避免死锁。

    在换乘电梯中,一个电梯到达换乘层时,需要等待另一个电梯的信号;

    在向电梯的 outRequest写入时,要及时唤醒电梯调度线程;

    在向 globalRequest写入时,要及时唤醒分配调度线程;

    在电梯reset后,要及时唤醒分配调度线程,判断是否可以结束进程;

  3. 共享资源保护: 使用了同步块来保护共享资源,如 outRequestglobalRequestelevators 等。这些对象在多个线程中被访问和修改,因此需要进行合适的同步以保证线程安全。

6.2 层次化设计

  1. 类的分离: 主要分为MainInputSchedulerElevator四个线程,分别承担开启线程、处理输入、分配调度、电梯运行。
  2. 方法的划分: Elevator 类中的方法根据功能被合理地划分,比如 Up()Down()Arrive()Open()Close() 等方法各自负责电梯的不同行为。这种划分使得代码逻辑清晰,易于理解。
  3. 类的细化:hw7将原本的Elevator类拆分为Elevator类、Operation类、Input类,将接收信号/运行、操作、输出分离,使结构清晰。

6.3 碎碎念

  1. 本单元作业要求挺好理解的,但由于对锁、线程的理解不到位,导致出现了各种各样的问题,学习至此,也只能说理解了个七八分,比如我只使用了synchronized这一种锁,也只使用了修饰代码块一项功能,以后还是要继续探索。
  2. 相比较第一单元,本单元的优化出现了百花齐放的情况。hw5那周我还在为两个arrive之间不能恒等于0.4s而耿耿于怀、还在为某种情况明显慢于同学而做优化,到现在则慢慢明白了盲目追求局部最优没有意义。输入千变万化,时间、分配策略、运行策略的优劣难以权衡,如今回望,尽己所能考虑即可、努力过无愧于心就好。
  3. hw6那周清明想出去玩、整个人是一种很浮躁的状态,wa了两个点倒也是应得的,但hw7很认真的写了、测试了,还是出现了一个TLE,而且是很浅显的错误,所以hw7强测结果出来挺难过的。不过这就是编程的魅力吧,差之毫厘,谬以千里,也算是长个教训,告诫自己写之前多思考、写之后多测试。hw7TLE是调度的问题,但debug时没有采取暴力地写一个均分(感觉这样太对不起后两周花在调度上的心思了),而是又倔强的想方设法在五行之内优雅地de完了bug,以此为电梯这一单元画上一个勉强的句号。
  4. 上一单元立了个flag,想开始写自己的测评机,这一单元迈出了小小的一步,下一单元希望自己能再接再厉!
...全文
177 回复 打赏 收藏 转发到动态 举报
写回复
用AI写文章
回复
切换为时间正序
请发表友善的回复…
发表回复

301

社区成员

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

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