BUAA ObjectOriented Unit2总结

BUAA ObjectOriented Unit2总结

​ 总的来说,第二单元的作业是通过\(Java\)的多线程来模拟北航新主楼的电梯接人。第一次作业中,每栋楼只有一部纵向电梯;第二次作业中,除每栋楼初始的一部纵向电梯外,还可以新加横向电梯和纵向电梯;第三次作业中,需要实现乘客在不同的纵向电梯及横向电梯之间的换乘功能。总体而言,三次作业的代码量还是非常小的,在每次写的时候也没有出现导致写不下去的致命\(bug\),但是由于种种原因,前两次作业踩了大坑,痛失分数,体验极差,只有第三次作业吸取血的教训,正常通过。

第一次作业

代码部分

架构分析

  • 总框架

    ​ 由于每一栋楼都只有一部纵向电梯,因此,在这次作业中我没有写调度器,而是在输入线程中直接将请求分发给每一部电梯的请求队列中,而每部电梯在可以上下人的时候就会遍历已处于电梯中的人和该电梯的请求队列,根据已处于电梯中的人和该电梯的请求队列进行上下人

  • 电梯框架

    ​ 当整个程序还没有结束时,电梯一直不断地进行一套行为,这一套行为为:\(判断上下人\rightarrow实现上下人\rightarrow输出开关门和上下人信息\rightarrow调整电梯方向\rightarrow电梯移动\),由于我采用的电梯调度算法为\(look\)算法,且运行过程中不会使用其他的调度算法,因此,我没有实现指导书推荐的策略模式,而是把\(Strategy\)类所要完成的工作直接写在了\(Elevator\)类中,让电梯一直按照写好的策略运行。电梯运行的大致代码如下:

    public class Elevator extends Thread {
        public void run() {
            while (true) {
                synchronized (requestTable) {
                    pickoff();//判断下人
                    pickup();//判断上人
                    getchange();//调整电梯方向
                    getprint();//输出开关门和上下人信息
                }
                move();//电梯移动
            }
        }
    }
    
  • 同步块的设置和锁块的选择

    ​ 本次作业有且只有一个共享对象,就是每一部电梯的\(requestTable\),这个对象在\(Elevator\)类和\(InputHandler\)类中会存在冲突,所以要在这方面设置相应的锁和同步块。

    • \(RequestTable\)类中是仿照了实验的代码,将每个方法都加上了锁,并在每个方法的最后都进行了\(notifyAll()\),当时我就觉得,好像不用每个方法都加\(notifyAll()\),有些方法加不加都行,但考虑到这是实验给出的代码,觉得还是养成加\(notifyAll()\)习惯吧,结果没想到给第二次作业挖了一个大坑。。。

      public class RequestTable {
          public synchronized void add(PersonRequest request);
          public synchronized void remove(PersonRequest request);
          public synchronized void setEnd(boolean isEnd);
          public synchronized boolean isEnd();
          public synchronized boolean isEmpty();
          public synchronized HashSet<PersonRequest> getRequests();
      }
      
    • \(Elevator\)类中采用的是生产者消费者模式,只要等待队列中还有请求没有处理,或者生产者的输入还没有结束,消费者就一直进行循环,每次循环开始都会判断等待队列中的请求和已处于电梯中的人是否同时为空,如果同时为空,就\(wait()\)在该电梯的\(requestTable\)上,否则就进行设置好的电梯的那一套行为,而当电梯从\(wait\)状态下被唤醒后发现生产者的输入已经结束,就可以直接\(return\)结束此线程了,否则就会进行设置好的电梯的那一套行为。

      public void run() {
          while (true) {
              synchronized (requestTable) {
                  if (requestTable.isEmpty() && persons.isEmpty()) {
                      if (requestTable.isEnd()) {
                          return;
                      }
                      try {
                          requestTable.wait();
                      } catch (InterruptedException e) {
                          e.printStackTrace();
                      }
                  }
              }
              //do something
          }
      }
      

      这里其实只需要明确代码运行的逻辑和代码什么情况下结束的逻辑就好了,但值得一提的是,此时的\(wait\)状态只能够被\(RequestTable\)类中的\(setEnd()\)方法和\(add()\)方法唤醒,而由于我使用的是实验中提供的代码,\(RequestTable\)类中的每一个方法都会进行\(notifyAll()\),虽然在一个\(requestTable\)只会对应一部电梯的第一次作业中不会出现问题,最坏的情况下也只会导致\(cpu\)时间多一点,但在之后的作业中,如果多部电梯共享同一个\(requestTable\),就会出现轮询问题。

性能优化

  • 电梯采用的是\(look\)策略,而非指导书的基准\(ALS\)策略,我收集了一下使用\(ALS\)策略的同学的数据,发现\(look\)策略确实要比\(ALS\)策略会更快的,而且现实生活中使用的也是\(look\)策略,想必也是因为\(look\)策略确实有其优势吧

    • 电梯运行方向(初始方向为向上):当电梯中有乘客时,方向为电梯中乘客的目的地方向;当电梯中没有乘客时,先检验当前电梯运行方向上是否有请求,如果有,就保持原方向不变,否则会调转方向
    • 电梯稍带策略:只接目的地与电梯当前运行方向相同的外部乘客
  • 在开关门的\(400ms\)间隔内可以随时接人(毕竟指导书中都说了是纸片人),做法就是,这\(400ms\)使用\(wait(400)\)方法,如果发现被唤醒后没有\(wait\)\(400ms\),则继续\(wait\)剩余时间,重复上述操作直至\(wait\)\(400ms\)

    while (System.currentTimeMillis() - lastopen < 400) {
        if (System.currentTimeMillis() - lastopen >= 400) {
            break;
        }
        requestTable.wait(400 - (System.currentTimeMillis() - lastopen));
    }
    
  • 在某些情况下可以接两次人,即在\(look\)策略中,如果要掉头,则立即再接一次人。这个细节处理的情况是,如果电梯在某一层开门后进行了下人的操作,此时发现当前层没有同方向请求,但是当前层有反方向请求,且反方向更远的楼层也有请求,此时如果掉头后没有进行立马再接一次人的操作,就会接反方向更远的人,当前层的就不会管了。

    public void getchange() {
        if (persons.size() == 0 && !hasnext(up)) {
            up = !up;
            pickup();//掉头后立马再进行一次接人操作
        }
    }
    
  • 接人时进行排序,如果发现当前层的同方向请求有点多,超过了电梯的上线,则会将要上电梯的人进行排序,按照一定标准进行接人,可以优先接目的地近的,也可以优先接目的地远的,两种做法互有优劣,但由于排序会导致接的人的目的地更加接近,能让电梯在短时间内大规模地上下人,性能一定会比不排序的更好。

    public void choose(int i) {
        if (getonpersons.size() > i) {
            getonpersons.sort();//按照一定规则进行排序
        }
    }
    
  • 量子电梯瞬移一层,当电梯处于\(wait\)状态时,如果我在等待的时候获取一个时间戳,等待结束再获取一个,记录等待了多少时间,由于等待结束会有请求要处理,如果要处理的请求不在当前层并且等待时间超过了\(400ms\),那么就可以瞬移一层(鬼知道你电梯在\(wait\)的时候是不是也在运行),而不用再等待电梯移动一层的时间,不满\(400ms\)也可以减少一点时间移动的时间,但是和舍友对比了一下这个优化有可能也是负优化,因为其实如果你瞬移了,那么如果当前层过了几毫秒来的请求就接不到了。

    public void run() {
        lastmove = System.currentTimeMillis();//wait之前记录一个时间戳
        requestTable.wait();
        if () {
        	quantummove();//如果是从wait状态被唤醒,使用量子移动
        } else {
        	move();//否则使用普通移动
        }
    }
    public void move() throws InterruptedException {
        //do something
        sleep(400);//普通地睡移动所需时间
    }
    public void quantummove() throws InterruptedException {
        //do somthing
        long interval = System.currentTimeMillis() - lastmove;
        if (interval < 400) {//超过移动所需的时间,直接量子移动,否则睡不够的时间
            sleep(400 - (System.currentTimeMillis() - lastmove));
        }
    }
    

分析部分

类图分析

时序图分析

sequenceDiagram participant M as Main participant I as InputThread Participant R as RequestTable participant E as Elevator activate M opt 初始化阶段 M->>+I: 创建启动输入线程 activate I M->>+R: 创建5个请求队列 activate R M->>+E: 创建启动5部电梯线程,同时与InputThread, RequestTable绑定 activate E end opt 处理请求 I->>R: 添加请求addReqeust E->>+R: 获取请求队列 R->>-E: 返回请求队列 E->>E: 开门 E->>E: 下客 E->>E: 生成要接送的请求列表 E->>+R: 获取请求队列 R->>-E: 返回请求队列 E->>E: 返回乘客列表 E->>E: 上客 E->>R: 从请求队列中删除已上乘客请求 E->>E: 关门 E->>E: 请求下一步行动方向 E->>+R: 获取请求队列 R->>-E: 返回请求队列 E->>E: 返回下一步行动方向 end opt 线程结束 I->>R: 传递输入结束信号 deactivate I R->>E: 传递输入结束信号 deactivate R E->>E: 待当前所有请求处理完毕后结束 deactivate E end deactivate M

测试部分

自测

​ 由于此次作业还算是比较简单(并且是放假期间比较偷懒),于是我没有做任何的测试,也没有写评测机,发现能过弱测就没管了,当然由于第一次作业还是比较简单,强测互测没有出除了输出线程安全外的功能性错误

互测

​ 互测由于没有考虑输出的线程安全问题,导致因为这个被刀了好几次,强测则是由于自己作死,想着优化优化,结果导致很多点都非常慢,甚至还\(TLE\)了三个点,实在是出大问题。

  • 对于线程安全问题,由于我没有考虑到官方指导书给出的输出包是线程不安全的,导致输出的时间戳不是递增的,具体原因是,如果有两部电梯均需要进行输出,其中的一步电梯输出函数刚获得时间戳后,\(CPU\)就去执行另外一部电梯的输出,导致先获取时间戳的电梯要比后获取时间戳的电梯还要靠后输出,解决方法是,将官方的输入封装一下,让输出成为原子操作。

    public class OutputThread {
        public static synchronized long println(String msg) {
            return TimableOutput.println(msg);
        }
    }
    
  • 对于自己瞎优化的问题,因为看到中侧中都是非常小的数据,导致我错误地认为强测也会是非常小的数据,基本上都是40秒就可以跑完的那种,在这种小数据下,电梯绝大多数情况下是做不满人的,于是,我就让电梯同时接正方向的人和反方向的人(但还是优先接同方向的人),这样的话可以少开关一次门,节约一点时间,这种策略在中测中表现非常不错,每个点都能比只接同方向的人的策略要快上一两秒,但是没想到,强测每个点都是有着七十条数据,且大部分数据都是卡在了最后一秒投入,这样会导致接反方向的策略非常慢,甚至还让我\(TLE\)了三个点,只能说是自己作死了,也让我吸取了非常多的教训,让我成长了很多。。。

至于别人的\(bug\)嘛,实在是一言难尽,由于自己作死\(TLE\)了三个点,导致自己进了\(B\)类房间,同组的人都有着可以说是非常低级的\(bug\)了,像是虚空接人,无限电梯,根本停不下来之类的,没啥好说的,也没啥心情可说的。

第二次作业

代码部分

架构分析

  • 总框架

    ​ 这次作业相比于第一次作业来说,增加了横向电梯,并且可以在运行途中动态地增加电梯,指导书中给出的基准策略是平均分配,但是平均分配不够符合我的价值观。并且如果分配不能够整除呢?我该让哪部电梯获得那些“余数”呢?我一个强迫症患者可受不了。并且我觉得自由竞争要更好,谁抢到就是谁的,还有点贪心算法的味道。使用自由竞争策略的话,每一栋楼和每一层楼的等待队列都是共享的,并且这些等待队列之中没有任何的关系可言,不需要实现换乘啥的,所以我就没有写调度器,而是在输入线程\(InputHandler\)中就将输入请求分发给了每一个等待队列了。

  • 电梯框架

    ​ 虽然这次作业有两种电梯,并且横向电梯和纵向电梯有着非常大的差异,但我还是选择和第一次作业一样,电梯没有策略类,要执行的操作为写死在电梯里的一套,这也就导致我有两个独立的电梯类\(VerticalElevator\)\(HorizontalElevator\),并且这两个类中有些代码是非常相似的,但把它们抽象出来的话会非常麻烦,所以我选择复制粘贴。甚至两个电梯的核心代码都如下:

    public void run() {
        while (true) {
            synchronized (requestTable) {
                pickoff();//判断下人
                pickup();//判断上人
                getchange();//调整电梯方向
                getprint();//输出开关门和上下人信息
                whethertomove = hasnext() || !persons.isEmpty();//判断是否移动
            }
            move();//电梯移动
        }
    }
    
  • 同步块的设置和锁块的选择

    ​ 基本锁块和同步块和上次完全一样,但由于这次是自由竞争,有多部电梯共享同一个等待队列,就得让电梯是否移动的判断提前到同步锁块中,也就是我的这次核心代码部分要比第一次作业多了\(whethertomove\)这个东西的原因,并且将判断前置也不会影响代码的正确性或者是效率,因为就算此时判断了不移动,在下一个循环还是会进一步判断的。

性能优化

  • 横向电梯使用类似于\(look\)的策略

    将乘客的目的方向定义为完成这个请求的最短路线的运行方向。

    • 电梯运行方向:当电梯中有乘客时,按照电梯中乘客目的地的方向;当电梯中没有乘客时,电梯向最近的请求移动。
    • 电梯接人策略:当电梯中有人时,只接同方向的人;当电梯中没有人时,接主请求方向的所有人,其中主请求为目的地最近的请求
  • 横向电梯与纵向电梯均使用自由竞争策略

    ​ 当有请求加入到等待队列中时,所有的电梯都回去争抢,谁抢到就是谁的。但从最后的结果来看,其实平均分配和自由竞争对性能没有什么太大的影响。

分析部分

类图分析

时序图分析

sequenceDiagram participant M as Main participant I as InputThread Participant R as RequestTable participant E as Elevator activate M opt 初始化阶段 M->>+I: 创建启动输入线程 activate I M->>+R: 创建5个请求队列 activate R M->>+E: 创建启动5部电梯线程,同时与InputThread, RequestTable绑定 activate E end opt 添加电梯 I->>E: 接收添加电梯的请求 E->>+R: 建立相应电梯的等待队列 activate R E->>E: 创建启动相应电梯线程,同时与RequestTable绑定 end opt 处理请求 I->>R: 添加请求addReqeust E->>+R: 获取请求队列 R->>-E: 返回请求队列 E->>E: 开门 E->>E: 下客 E->>E: 生成要接送的请求列表 E->>+R: 获取请求队列 R->>-E: 返回请求队列 E->>E: 返回乘客列表 E->>E: 上客 E->>R: 从请求队列中删除已上乘客请求 E->>E: 关门 E->>E: 请求下一步行动方向 E->>+R: 获取请求队列 R->>-E: 返回请求队列 E->>E: 返回下一步行动方向 end opt 线程结束 I->>R: 传递输入结束信号 deactivate I R->>E: 传递输入结束信号 deactivate R E->>E: 待当前所有请求处理完毕后结束 deactivate E end deactivate M

测试部分

自测

​ 此次由于第一次的经验教训,于是和两个舍友一起写了评测机,我主要是负责数据生成,解析输入等等,投喂什么的可以使用官方包,我们写了三个数据生成器,一个是纯随机的一次生成400条左右进行测试,还有就是横向电梯和纵向电梯分别的压力测试(比如第一次强测那样70s来一堆,什么电梯刚走这层就来一堆请求这种)

互测

​ 对于这次的强测无话可说,因为之前是模仿的助教的代码,而在第二次作业中没有进行修改,导致互测都没进,原因是多了两个\(notifyAll()\)。轮询的情况为:有两部电梯共享同一个队列(当前队列里面没有请求),当恰好一部电梯进入循环拿到了 \(requestTable\)的锁之后(另外两部此时在 \(wait()\)),它就会进入第二个 \(while()\)块判断,而此时 \(isEmpty()\)\(isEnd()\)都是 \(requestTable\)里面的方法,都 \(notifyAll()\)了,所以它唤醒了另一部电梯和输入线程,而它就进入了 \(wait\),另一部电梯从 \(wait\)醒来还抢到了锁,于是它又进入 \(while\)的判断于是又将第一部电梯唤醒,这样就可能进入死循环(输入线程一直抢不到锁),就算没有进入死循环都相当于在一直轮询(因为每次 \(isEmpty\)\(isEnd\)唤醒后输入线程和电梯线程就开始抢锁,经测试有些情况输入线程抢到锁的概率会很低,所以会循环挺长一段时间),而只要将这里的两处 \(notifyAll()\)删了就没有问题了,而考虑什么时候需要 \(notifyAll()\),其实只有两种情况,一个是 \(add()\)另一个是 \(setEnd()\)

​ 其实在第一次作业时候我就已经考虑过助教的代码里面 \(notifyAll()\)加的有点多,过量的 \(notifyAll\)反而会增大开销,但是由于当时刚学多线程,觉得助教代码是一个好习惯(因为一直强调不要忘了唤醒线程),到了第二次作业的时候,就忘了这件事了,也根本不会去修改\(requestTable\)中的代码,更何况本地和中测都没有出现这种\(bug\),导致\(cpu\)时间报表,几乎全军覆没。

第三次作业

代码部分

架构分析

  • 总框架

    ​ 此次作业相比于第二次作业来说,需要支持换乘,也就是在请求的目的楼层,出发楼层,目的楼,出发楼完全没关系时,需要乘坐多部电梯才能到达目的地,于是,不同的请求队列之间就有牵扯了,需要将请求从某一个请求队列中取出放到另一个请求队列中。于是此次作业我加入了三个调度器,一个负责所有的纵向电梯,一个负责所有的横向电梯,还有一个负责总的调度,为了便于实时获取,这三个调度器别设置成了单例模式,其次,还设置了缓冲队列,这个缓冲队列和等待队列的作用类似,用于消除取出请求和放入请求之间的制约关系。

  • 电梯框架

    ​ 本次作业对比于上次作业来说,基本没有什么改变,只是新增加了一个\(feedback()\)方法,用以处理换乘。

    //纵向电梯的feedback
    public void feedback() {
        for (MyPersonRequest request : getoffpersons) {
            if (request.isTransferred()) {
                WaitQueue.getInstance().decrease();//已换乘过,完成请求
            } else {
                //do something
                WaitQueue.getInstance().add(request);//还未换乘,将其放回缓冲队列
            }
        }
    }
    //横向电梯的feedback
    public void feedback() {
        for (MyPersonRequest request : getoffpersons) {
            //do something
            WaitQueue.getInstance().add(request);//横向电梯起换乘作用,一定会将其放回缓冲队列
        }
    }
    
  • 调度器框架

    ​ 在总调度器拿到缓冲队列的锁时,就会一次性调度完缓冲队列中的所有请求。

    public class Scheduler extends Thread {
        private static final Scheduler SCHEDULER = new Scheduler();
        public static Scheduler getInstance();
        public void run() {
            while (true) {
                synchronized (WaitQueue.getInstance()) {
                    for (MyPersonRequest request:WaitQueue.getInstance().getRequests()) {
                        if (request.isTransferred()) {
                            //do something
                        } else {
                            //do somthing
                        }
                    }
                    WaitQueue.getInstance().getRequests().clear();//处理完后清除
                }
            }
        }
    }
    
  • 线程结束框架

    ​ 此次作业的线程结束是一个难点,细说下来,它分为两个阶段的判断,首先是判断输入线程是否结束,输入线程结束后就告知\(Scheduler\),让其判断输入的总请求数是否等于完成的请求数,如果相等,就调用横向电梯调度器和纵向电梯调度器的\(setEnd()\)方法告知每一个电梯结束。

  • 同步块的设置和锁块的选择

    ​ 此次作业根据两个有关方面进行了加锁,首先是和缓冲队列有关的方面,在\(WaitQueue\)类中,每个方法都加上了锁,并且为了避免像上次一样 \(notifyAll\)轮询,于是在 \(WaitQueue\)中采取, \(add\)\(notifyAll()\),当输入线程结束后, \(setEnd\)\(notifyAll()\),在完成一个请求后,如果发现完成请求数与输入请求数相同就 \(notifyAll()\),不多加任何的 \(notifyAll()\)

    public class WaitQueue {
        private static final WaitQueue WAIT_QUEUE = new WaitQueue();
        public static WaitQueue getInstance();
        public synchronized void add(MyPersonRequest request) {
            //do something
            notifyAll();
        }
        public synchronized void increase();
        public synchronized void decrease() {
            //do something
            if () {//如果发现完成请求数与输入请求数相同
                notifyAll();
            }
        }
        public synchronized void setEnd(boolean isEnd) {
            //do something
            notifyAll();
        }
        public synchronized boolean isEnd();
        public synchronized boolean isEmpty();
    }
    

    ​ 其次,由于我对请求做的是动态拆分,所以,每当我增加一部横向电梯时,就会对还没有进行换乘的请求刷新一遍拆分,而刷新时,需要遍历所有纵向电梯中的请求队列,所以需要锁住请求队列。

    public void flush(ElevatorRequest elevatorRequest) {
        for (Building building : buildings) {
            synchronized (building.getRequestTable()) {//锁住请求队列
                for (MyPersonRequest request : building.getRequestTable().getRequests()) {
                    if () {//如果新加的电梯更适合换乘
                        //do something
                    }
                }
                //do something
            }
        }
    }
    

性能优化

​ 和第二次作业一样横向和纵向都采用自由竞争,第二次作业有的优化,第三次作业中也有。

  • 输入时拆分请求,即写一个 \(MyPerSonRequest\)继承官方包里面的 \(PersonRequest\),同时记录换乘楼层,是否已经换乘,在被完成一次电梯运送后自动改变自己的 \(presentFloor\)\(presentBuilding\)...(通过重写一系列 \(get\)方法实现)

    public class MyPersonRequest extends PersonRequest {
        //每个变量的含义如其名
        private int nextFloor;
        private char nextBuilding;
        private int presentFloor;
        private char presentBuilding;
        private boolean transferred = false;
    }
    
  • 动态拆分请求,每增加一部横向电梯时,就会对还没有进行换乘的请求刷新一遍拆分,并将刷新后的请求重新放回缓冲队列中等待调度。

    public void flush(ElevatorRequest elevatorRequest) {
        flushed = false;
        flushedRequest.clear();
        for (Building building : buildings) {
            synchronized (building.getRequestTable()) {//锁住请求队列
                for (MyPersonRequest request : building.getRequestTable().getRequests()) {
                    if () {//如果新加的电梯更适合换乘
                        //do something
                        flushedRequest.add(request);
                        flushed = true;
                    }
                }
                for (MyPersonRequest request : flushedRequest) {
                    building.getRequestTable().remove(request);//在锁块中执行取出操作
                }
            }
        }
        if (flushed) {
            synchronized (WaitQueue.getInstance()) {//锁住缓冲队列
                for (MyPersonRequest request : flushedRequest) {
                    WaitQueue.getInstance().add(request);
                    //将刷新后的请求重新放回缓冲队列中等待调度
                }
            }
        }
    }
    

分析部分

类图分析

时序图分析

sequenceDiagram participant M as Main participant I as InputThread participant IQ as WaitQueue participant C as Scheduler Participant SC as Total_ Participant R as RequestTable participant E as Elevator activate M opt 初始化阶段 M->>I: 创建启动输入线程 activate I M->>IQ: 创建启动输入队列 activate IQ M->>C: 创建主调度器 activate C C->>SC: 创建5个楼栋调度器,10个楼层调度器 activate SC end opt 添加电梯 I->>IQ: 接收添加电梯的请求 IQ->>C: 传递添加电梯请求 C->>SC: 让相应调度器添加电梯 SC->>+R: 建立相应电梯的等待队列 activate R SC->>E: 创建启动相应电梯线程,同时与RequestTable绑定 activate E E->>E: 电梯创建自己的策略类 end opt 处理请求 I->>IQ: 添加乘客请求 IQ->>C: 传递乘客请求 C->>SC: 分配请求 SC->>R: 将请求按照分配策略放入等待队列中 E->>+R: 获取请求队列 R->>-E: 返回请求队列 E->>E: 开门 E->>E: 下客 E->>E: 生成要接送的请求列表 E->>+R: 获取请求队列 R->>E: 返回请求队列 E->>E: 返回乘客列表 E->>E: 上客 E->>R: 从请求队列中删除已上乘客请求 E->>E: 关门 E->>E: 请求下一步行动方向 E->>+R: 获取请求队列 R->>E: 返回请求队列 E->>E: 返回下一步行动方向 end opt 处理换乘 E->>E: 开门 E->>E: 下客 E->>E: 关门 E->>+IQ: 把换乘请求发回输入队列 IQ->>-C: 传递乘客请求 C->>SC: 分配请求 SC->>R: 将请求按照分配策略放入等待队列中 end opt 线程结束 I->>IQ: 传递输入结束信号 deactivate I IQ->>C: 告诉主控制器输入结束 C->>C: 接收电梯换乘请求,等待所有请求处理完毕 E->>IQ: 告诉等待队列请求全部处理完毕 deactivate E C->>SC: 发出结束信号 deactivate C deactivate IQ SC->>R: 传递结束信号 deactivate SC deactivate R end deactivate M

测试部分

自测

​ 这次作业同样也和室友写了评测机,但经过上次的打击后,发现光通过评测机检验正确性不够,还得看\(CPU\)时间有没有超,于是我使用了\(JProfiler\)来观测\(CPU\)时间,根据线程的阻塞情况,分析阻塞的原因,并减少阻塞时间,通过观察\(JProfiler\)的运行情况,我还对我的代码做了小优化。

互测

​ 强测和互测都没有出现\(bug\),强测得分\(99.7+\),感觉这才是我应有的水平,之前两次作业真的是一言难尽。

​ 刀中了很多人的\(bug\),其中一个 \(bug\),是如果当前有个横向请求,但是没有对应可以到达的横向电梯,他直接把人加在了当前层横向电梯的等待队列里面,于是该请求始终没有处理,所以只要有这种数据就会出错,这么严重的\(bug\)居然能进\(A\)房间,不禁让我觉得我第二次作业挂掉的\(19\)个点有点不公平,另一个\(bug\)是在\(HashSet\)循环的时候,没有锁住它,导致其他的线程会删除里面的元素而出问题,在运行中会报错,但最后的结果是对的,但是我把本地有50%概率复现的数据交上评测机,连交了三次也没有\(hack\)中。(所以这是我运气有亿点不好呢,还是说助教的评测机成精了只会针对我呢)

心得体会与收获

​ 总的来说,这一单元的难度属实不是太大,并且代码量也比较小,哪怕是最后一次作业也只写了\(900\)行左右,但由于是第一次接触多线程,导致很多地方都不是很清楚,直接仿照了助教的代码,导致因为\(notifyAll()\)出现了轮询问题,导致第二次作业挂了十九个点,虽说失败能让我学到东西,但这个代价也未免有些太大了。其次,第一次作业中,我瞎优化导致\(TLE\)了三个点,那次的教训也让我明白,不要根据弱测揣度强测,往大的方面来说,不要根据目前的状况来揣度未来的情形,自己的代码,自己的架构,不是面向现在的,而是面向未来的。

​ 最大的收获,就是学习了解了多线程,并且累积了一定的多线程\(debug\)经验,知道了多线程不应该只考虑正确性,还要考虑线程安全问题,轮训问题,阻塞问题,不仅仅需要大规模的数据测试,对于单组数据也需要多次运行检验线程安全问题。

posted @ 2022-05-04 13:41  praynext  阅读(26)  评论(1编辑  收藏  举报