BUAA_OO_Unit2总结

高悠然-22371242 学生 2024-04-20 15:51:32

OO第二单元总结

1、作业要求回顾

  • hw5:完成指定电梯分配和并发运行;

  • hw6:不对电梯进行指定,并加入reset请求,对电梯的参数可以修改;

  • hw3:电梯可以转变成双轿厢,相当于加入了一个共享楼层的处理。

2、hw5分析

2.1 UML类图

img

2.2 架构分析

第一次作业整体难度不大,给了我们规定的电梯号,主要考察的是对于程序的同步互斥的理解。从第一次作业到第三次,我都采用了统一的架构模型。

  • 首先是生产者-消费者模型,inputhread作为输入线程,其实就是请求的产生者。我将请求作为元素存到容器requestable中,传给消费者调度器(或者电梯),也因此请求成了我们的第一个共享对象。

  • 其次是状态模式,我将电梯看成一个状态机,其状态的改变只和其自己内部有多少人,外部有多少等待人有关,分析后做出状态改变。

  • 最后是单例模式,电梯有很多个,但调度器只有一个,只实例化一个调度器用于调度。
    所以最后除去main线程,我一共实例化了8个线程,共享对象只有请求队列一个。

2.2 电梯运行策略

借鉴了往年的blog,我采用的是lock算法,其运行策略如下

  • 检测当前楼层是否有人上来或下去,即等待序列和电梯里的序列是否有在当前楼层下的,进行开门进入出去和关门

  • 接下来如果电梯里有人则在电梯的规定方向上移动一层

  • 如果电梯里没人,且等待序列为空,序列未结束,则电梯等待;若结束,该电梯线程结束

  • 如果电梯没人,且序列不为空,则先沿电梯运行方向查找是否有人,如果有则运行一层,如果无,则转向
    总的来说,该策略是优先处理同方向的乘客,和我们的平常的电梯类似。以下是策略状态转换代码:

    openDoor();
    
              if (curNum != 0) {
                  move();
              }
              else {
                  if (requestTable.isEmpty()) {
                      if (requestTable.isEnd()) {
                          return;
                      } else {
                          requestTable.myWait();
                      }
                  } else {
                      if (changeDir()) {
                          direction = !direction;
                      } else {
                          move();
                      }
                  }
    
                  if (requestTable.isEnd() && requestTable.isEmpty()) {
                      return;  //处理结束,直接关闭
                  }
              }
    
          }
    

2.3 线程结束条件

  • input:输入读到NULL

  • 调度器:等待序列为空,且已经结束

  • 电梯:自己的等待序列为空,且已经结束

    3、hw6分析

    3.1 UML类图

    img

    3.2 架构分析

  • 本次作业加入了reset请求,在本次作业中,我将reset看成了和人的请求同等级的请求,通过一个request类继承,一起放入请求对列(但其实有点问题,下一次进行了修改)。

  • 其次我将reset看成电梯的一个状态,当有reset进来时,电梯的重置位置1,然后当电梯运行发现为1时,开始reset。

  • 最后我对电梯的结构进行了重构,因为本次作业调度器并不知道分配给哪一部电梯,需要根据电梯的状态来判断,因此电梯的状态实际算是共享对象,由电梯自己和调度器共享,为了方便上锁,我将电梯属性拿出来,变成Ele类,而电梯类则变成了一个运行的空壳,通过Ele的状态进行run。

3.3 电梯运行策略

整体和上次变化不大,主要是加入了一个reset状态的改变,我的reset实现分为两步,首先驱赶电梯里的人,然后对电梯的状态进行修改,在休眠1200ms期间,通过将reset位置1,来告诉调度器电梯正在reset,不能分配。状态转换代码如下:

public void run() {
        while (true) {
            if (ele.isReset()) {
                reset();
            }
            else {
                openDoor();
                if (ele.isCurNum()) {
                    move();
                } else {
                    if (ele.isEmpty()) {
                        if (ele.isEnd()) {
                            return;
                        } else {
                            ele.myWait();
                        }
                    } else {
                        if (ele.changeDir()) {
                            ele.tDir();
                        } else {
                            move();
                        }
                    }

                }
            }
        }

    }

3.4 电梯调度策略

我并没有实现影子电梯,电梯调度策略我写了很多个方法,但如果不是完全去模拟请求队列,其实效果都差不多,因为你无法知道下一个请求是什么,它对于当前的性能改变是影响很大的。
我优先处理捎带,如果不能捎带,则随机分给不在reset的电梯。最后试了一下,静态分配,平均分配,随机分配,或是加入较为简单的评分最后效果都差不多。
这样写的好处应该就是代码量很少,且能取得还可以的性能分。不过影子的想法明显是更好的,不过由于个人能力和时间问题,未能实现,希望以后能再尝试。

3.5 线程结束条件

  • input:NULL

  • 调度器:因为reset的乘客会返回队列,所以它的结束不能只看input,还得看电梯,我加入了一个新条件,即没有电梯在reset,可以发现一旦电梯没有reset,且输入队列结束,不会有乘客返回请求队列。

  • 电梯:队列为空且结束

    4、hw7分析

    4.1 UML类图

    img

    4.2 架构分析

  • 首先我对reset的优先级进行了调整,其实结合日常,我们也能感觉到,电梯维修比人的请求要高级。因此我的reset会在input进来后,直接处理,不会进入等待序列,但在电梯中还是以状态的形式存在。

  • 其次由于双轿厢的加入,我的电梯线程最多可以开到12个,我采用的方法是在电梯内部开,当读到双轿厢请求时,在电梯的reset里创建一个新的电梯属性和运行类,对其进行start,也因此电梯也要获取总的电梯队列。所以电梯属性队列也成了共享对象,我新开了一个elevatortable类,用来进行上锁与共享。

  • 共享楼层Flag的加入,我将其作为一种可知信息,传到电梯运行内部,根据其状态,来判断电梯是否能进入共享楼层

4.3 电梯运行策略(如何处理共享楼层)

结构和上次差不多,主要是加入了对于共享楼层的处理。我这里参考了讨论区姜涵章同学的做法,设置了一个flag作为标志,电梯进入共享楼层,置为occupied,离开为release。如果楼层为occupied,则另一个电梯等待其离开。代码如下:

public class Flag {
    private boolean occ; //ture:occ

    public Flag() {
        occ = false;
    }

    public synchronized void setOccupied() {
        waitRelease();
        occ = true;
        notifyAll();
    }

    public synchronized void setRelease() {
        occ = false;
        notifyAll();
    }

    private synchronized void waitRelease() {
        notifyAll();
        while (occ) {
            try {
                wait();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

因为电梯可自行从共享楼层移动走,因此我在运行策略类加入了一个判断,如果电梯到达共享层,且方向会超过目标楼层,立马进行转向,同时如果电梯里为空,等待队列为空,电梯立马离开共享楼层,避免长时间占用,保证安全性。

4.4 电梯调度策略

仍然和上次差不多,且这次电梯数量更多,每个电梯的楼层限制也不同,我主要采用了随机分配给几号电梯,然后根据楼层,决定分配给A还是B电梯。
代码如下:

                Random rand = new Random();
                int id = rand.nextInt(6) + 1;
                if (numReset() >= 5) {
                    requestTable.addRequest(request);
                    try {
                        sleep(500);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                    continue;
                }
                try {
                    sleep(10);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                while (elevators.getOne(id - 1).isReset()) {
                    id = rand.nextInt(6) + 1;
                }
                int index = findModel(((Person) request),id);
public int findModel(Person person,int id) {
        int n = 0;
        for (int i = 0;i < elevators.getSize();i++) {
            if (elevators.getOne(i).getId() == id) {
                if (person.getFromF() == elevators.getOne(i).getTransferFloor()
                        && person.getDir()) {
                    if (elevators.getOne(i).getModel().equals("B")) {
                        n = i;
                        break;
                    }
                }
                else if (person.getFromF() == elevators.getOne(i).getTransferFloor()
                        && !person.getDir()) {
                    if (elevators.getOne(i).getModel().equals("A")) {
                        n = i;
                        break;
                    }
                }
                else {
                    if (elevators.getOne(i).StartFloor() <= person.getFromF()
                            && elevators.getOne(i).EndFloor() >= person.getFromF()) {
                        n = i;
                        break;
                    }
                }
            }
        }
        return n;
    }

4.5 线程结束条件

  • 调度器:因为电梯到达共享楼层,如果乘客未到达目的地,需要将其放入等待队列,因此只要所有的电梯都没有会跨过共享楼层的请求时才会停止。

    5、三次迭代分析

    5.1 调度器设计

    每次调度器设计在上面已经说过,总体结构不变,一直变化的是调度器的结束条件,以及调度策略的调整,同时我对reset的优先级进行了调整,从调度器撤出,不再由其分配。
    不过要注意调度器的等待条件,其实抽象的来说,就是等待队列为空,但没结束,这时候调度器需要等待。而等待队列的来源有三种:输入,电梯reset和共享楼层下人。所以针对自己的结束条件,将后面的结束取非即可。

    5.2 调度策略

    我的调度策略很简单,简单评分特判,大部分随机,因此没有在这上面消耗特别多的代码量,不过也导致了我的性能较为低下和看脸。

    5.3 锁和同步块设置

    我这次主要设置了方法锁,并没有设置同步块(感觉没有什么方法是被共享的,主要是对于方法里共享对象的修改上锁)。
    接下来我按照我的共享对象来叙述:

  • 等待队列:调度器和input的总请求队列,调度器和电梯的电梯等待队列。他们的锁我都上在了requestable这个类里面,对其的操作直接使用自己的锁

  • 电梯队列:主要是第三次,电梯里面需要对电梯队列加入新的电梯线程,对于加入元素,删除元素,获取某一个元素等都上锁

  • 电梯属性:主要是调度器需要获取电梯的属性,电梯要根据自己的属性进行运行,input需要对电梯的reset属性修改。我采用创建新的Ele属性类,并根据哪些属性被共享,对修改该属性和获取该属性的方法上锁。

    5.4 协作图

    img


    如图为多个线程之间的联系,以及他们连接的共享对象。

    6、bug分析

    6.1 自己的bug

    我的bug主要集中在第二次和第三次,第二次主要是一个性能的bug,第三次涉及到了死锁和轮询,以及一个特别睿智的bug。

  • 围攻一个电梯:因为我的程序如果电梯在reset就不会把请求分配给他,导致如果5个电梯都在reset,会把所有请求分给一个电梯,导致爆炸。我最后的解决是当该请求分配时,电梯reset数量大于等于5,则将该请求放回队列,调度器休息500ms后,再分配。

  • 迭代的失误:我在迭代的时候,对于上一个bug的修复,我按理说会把那个request重新放回,这次不进行处理,但我少抄了一个continue,导致当电梯reset数量过多时,会处理两次,导致wa了几个点,希望以后检查更仔细点,以及测试的全面性。

  • 轮询的造成:我在写第三次作业前,是坚定不移的一旦上锁必定notifyall的人,但第三次由于调度器的结束条件改变,,如果我上锁的方法全部都会notifyall,导致沉睡的电梯一睡就醒造成轮询。最后的处理方法是对锁的添加精细化,一些get方法,不需要notifyall,主要是对于写的方法,除非一些get方法涉及到了需要结束等待的要求。

  • 共享对象读写同时发生:主要是一开始我没有给电梯数组上锁,导致调度器会访问,而电梯可能会修改,造成线程不安全,修改方法我在第三次作业分析中已经介绍。

6.2 debug的方法

这里主要是说一下我对于轮询和死锁的debug方法。

  • 轮询可以采用print方法,在while里随便打印标识符,如果打印出的行数远大于所需要的,说明进入了轮询,未合理wait或sleep。

  • 死锁(或者多线程未唤醒):除了直接看代码外,也可以使用print,对wait或者一些上锁的地方,加入标识符打印,如果停在某一个标识符,则说明在那一处发生死锁。在这过程中很难的一部分,是对于死锁的复现,可能很多次才复现一次,而那一次你还不一定能知道原因。所以在写的时候,还是细心一点,对于锁的使用更加谨慎精细,而不是像我一样,无脑添加。

7、心得体会

这一单元的每一次作业我都写得很痛苦,远超上个单元的第二次作业。虽然我的调度很简单,但是由于个人对于锁的理解不够透彻等,很多时间花在了研究线程间运行的协作关系上。也导致了这次的代码量对我来说其实远小于上次,但最后我却基本要花三天整才能完成一次。以后还是要注意基础理论知识的学习,而不是机械地完成任务。
不过这次痛苦的经历,也让我看到了多线程的魅力与高效。让我对于计算机的并发也有了更加具象的理解。希望能为以后留下经验,更好地运用。

...全文
36 回复 打赏 收藏 转发到动态 举报
写回复
用AI写文章
回复
切换为时间正序
请发表友善的回复…
发表回复

301

社区成员

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

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