OO第二单元总结-电梯调度

综述

总体采用生产者-消费者模式,三次作业总体架构没有大的变化,只在每次新增任务时修改相关的细节。每一个请求处理流程:输入线程->调度器->电梯

  • 电梯,实现基本移动和电梯内状态查询,有配套的电梯队列

  • 策略,控制电梯移动

  • 调度器,从总请求池中分配请求至电梯队列

同步块设置和锁的选择

第一次作业相对比较简单。但由于对于多线程的不熟悉,关于如何设置同步也思考了很久。分析线程访问,主要是针对于存储request的容器需要同步控制,输入线程写入、调度器取出会进行写操作,经过分析之后,对于如何控制同步块就有了眉头。

生产者-消费者模式,输入线程往请求池中放入请求,消费者需要判断在请求池为空时等待,由于涉及到访问状态,需要synchronized保护访问。

 synchronized (waitTable) {
     if (waitTable.noWaiting()) {
         try {
             waitTable.wait();
        } catch (InterruptedException e) {
             e.printStackTrace();
        }
    }

以及为了保证共享数据的正确性,有容器(请求池)中的synchronized方法,例如

 public synchronized Request getRequest();
 public synchronized void delRequest();

但是这样做还有一些问题,就是消费者如何知道生产者已经停止了呢?这里一开始大致想了两种方法,一是生产者离开时放入一个特殊请求-1 to -1(poison)当消费者获得该请求时停止线程,二是增添请求池状态。后来上机时的架构感觉更加易于操作(但似乎有bug?),最后也采用了这种方法,生产者得到停止信号后关闭候乘表类,消费通过候乘表获得相关信息。

 synchronized (waitTable) {
     if (request == null) {
         waitTable.close();
         waitTable.notifyAll();
    }
 }

候乘表关闭状态,消费者可查询得到生产者已经结束生产的消息。

第二次、第三次作业的同步控制部分。由于每个电梯的访问是被保护的,所以多个电梯也大体类似,主要的地方是考虑到新加入电梯可以进行优化、换乘时调度器对于请求的支配能力,调度器需要实现把电梯中一部分请求迁移至新加入的,或者更加合适的电梯中,需要设置同步。

 synchronized (elevatorQueues) {
     // redraw() or dispatch();
 }

调度器设计

三次作业的调度器是一个迭代开发的过程。

第一次作业。由于只有一部电梯,主要是为了后续扩展性,没有太多的控制。

第二次作业。首先是如何分配请求的问题,主要考虑两个方面,一是电梯所在楼层,二是电梯内部人数。这两者有一定的权重,举个例子,如果电梯离某个请求很近,并且电梯内人数很少,调度器会把该请求分给这部电梯。但是如果电梯内人数很多,经过加权可能不如另一个距离较远的空闲电梯,则调度器会分给空闲电梯。新增电梯也可在此基础上进行调度,抽取工作权重较大电梯的请求,放入工作权重较小的请求,让每部电梯负载均匀。

调度器需要对请求池、电梯队列进行交互和相关状态的查询,最终进行分配。具体来说是

  • 输入线程得到输入后放入请求池,唤醒调度器

  • 调度器从请求池中取出请求,调用每一部电梯的状态查询方法,根据每个电梯不同状态以及请求的起点和终点,经过一定的加权选择之后将该请求分配给一个最为合适的电梯队列中。(或者是电梯请求,也进行相关的查询和分配)

至此,调度器的工作结束,问题就变成了单个电梯的运行。

第三次作业。有换乘的需求,所以调度器需要决策请求是否需要换乘,这方面我只进行了较为简单的优化。首先自然是根据需求分配最匹配的电梯,例如1-20这种优先考虑分配给C,在此基础上

  • 奇偶换乘,并且距离较远(大于设定阈值,作业中采用8层),A-B换乘

  • 长途运输,例如from 5 to 20,会根据当前情况先到1-3层中转,之后由C电梯处理

总体上来看对于大部分数据的性能较好,但是对于一些极端的数据,虽然有动态的加权计算,但是整体上由于策略较为固定,容易产生“虽然进行了良好的分配,但总时间却增加”的情况。

架构设计

第一次作业

UML类图

image

sequence diagram

image

 

第二次作业

UML类图

image

sequence diagram

image

第三次作业

UML类图

image

image

可扩展性

架构设计上分为输入、电梯、策略、调度器四部分。电梯部分只进行简易移动,对于三种模式配置三种策略类适应调度。对于不同种类的电梯区分,主要在于调度器的判断,电梯只是一个电梯,并不关心其他东西。如果需要实现功能扩展,一般不需要改动电梯,需要改变电梯的相关配置类、调度器。

  • 对于不同的模式,配置不同的Strategy

  • 对于不同电梯的调度、换乘请求,修改调度器以分配至不同的电梯队列。

虽然这样带来了比较好的扩展性,但还是有一定不足,例如调度器类承担了太多工作,存在的耦合较高等等,在后来课堂上讲解设计原则时,对比发现自己的设计还能做得更好。对于策略模式,相比于使用大量的if else,使用策略模式可以降低复杂度,使得代码更容易维护。但缺点是需要有很多的策略类,使得整体复杂度变高。

性能设计方面,性能方面还有很多可优化的地方。对于一部电梯而言,由策略驱动电梯运转,策略只关心电梯队列中的请求如何选择。

Random模式
  • 并不是标准look电梯,而是根据电梯位置、电梯状态对工作负载进行加权计算,选择一个工作权值较小的电梯,在一定范围内可以反向接人之后继续前进。

Morning模式
  • 根据可支配电梯的目前状态,集齐较多人数之后再启动。

Night模式
  • 先上到请求的最高层,一趟带入底层。

对于多部电梯,需要调度器考虑请求池中请求分配问题,以及是否需要对该请求采取换乘策略。如上文所述,核心可以概括为两种,奇偶换乘高低换乘。由于采取了较为细致的判别方式,所以调度器需要查询电梯状态、电梯队列的情况,对每个电梯遍历之后才能做出决策。在可扩展性方面,这并不是一个好的选择,基本上耦合了除输入线程以外的其他核心类,使得调度器不利于维护。

这样考虑优化实际上有些死板,策略较为固定,面对动态的数据难免出现不足,但在基本都有不错的性能分,不会出现太差的情况。研讨课对于算法的讨论很多,动态计算、打分等等方法或许在面对不同数据有更好的适应性。

分析自己程序的bug

第一次作业

第一次作业较为简单,公测和互测中未发现bug

第二次作业

第二次作业出现bug主要在某种执行顺序下,新电梯直接进入等待。原因主要在于调度器如果和一个电梯之间没有任何交互,在重新分配人员时如果该电梯还是没有分到请求,将会睡眠,最后调度器结束时没有唤醒该电梯。

核心代码段在电梯类的run()方法中,判断线程停止条件

image

 private boolean needWait() {
         return processingQueue.isEmpty() && elevatorQueue.isEmpty() && !elevatorQueue.isEnd();
    }

可以看到,run()方法相对复杂,在判断停止的逻辑也比较复杂,导致疏忽了判断。

首先分析问题出现的场景,新建造的电梯产生时,各电梯的权重非常均匀,导致新没有和调度器进行交互,直接进入等候状态,调度器后续也不对该电梯进行任何分配,直至结束。这样的结果是调度器结束了,但电梯还在等待。

如何解决这个bug呢?经过waitTableinputThread之间的交互的经验。同样采用了一个电梯队列的状态,当调度器结束时通知每一个电梯队列,这时即使新电梯没有经过任何调度,一旦收到了停止信号,醒来后就会检查电梯队列,在处理完毕后结束线程。

第三次作业

第三次作业的bug在于night模式的电梯分配问题,位于分配类的handlePriority()方法中,这是一个后来加入的针对优先匹配的请求的权重的分配方法。bug产生的原因在于night模式下进行了额外的优化选择,结果在优化时的疏忽,把电梯型号写错,导致触发优化条件的乘客,调度器会将这种优化的乘客放入错误的电梯型号,导致到达楼层产生错误。

互测未发现其他bug

发现bug的策略

如果通过静态分析,很难在不理解别人架构的情况下发现bug,所以采用测试数据测试。第一次作业中发现同组有1-2020-1连续出现时,最后一个乘客未被响应的bug,大概5次能复现一次。提交了一次,但没有复现,于是作罢。后续作业均只进行了一些测试数据检测,没有发现明显bug,由于其他课程的压力没有进行太多静态分析。

线程相关的问题,可以采用JProfiler插件来检测每个方法和每个线程的执行和运行状态,框定一个大致范围,可能还是需要进行静态分析来定位bug。与第一单元相比,测试的策略基本相似,主要通过测试数据进行黑盒实验,但相比之下,线程安全的bug可能并不容易复现,可能需要特定的状态和执行顺序才能触发。如果想要找到这类问题,需要仔细分析代码,所以在第二单元中对于代码的静态分析相对来说更为重要。

心得体会

永远要小心线程安全(没有发现bug不代表没有bug),多线程的并发带来了调试的困难,如果没有合理的同步控制,可能在某种情况下就出现了并发访问共享变量的的错误,或者不同线程先后拿到锁,但都在等待另一把锁的经典死锁问题。编写多线程代码时可以做好阶段性的log记录,如果遇到了难以复现的bug,有时难以调试,这时可以及时查看前一阶段的记录情况,合理分析从而减轻debug的工作量。

层次化设计。好的架构可以减少bug的产生,还能更好地进行迭代开发,进行相关优化。经过第一单元的学习,在第二单元的一开始的架构就构思了比较久,给后续的迭代开发留下了足够的空间,尽量将每个部分的功能分隔开,电梯只进行简单的运动,输入也只管输入,调度器负责调度,实现了每次作业的额外功能开发。但总体看来还是有一些不足之处,例如调度器的设计还是比较复杂,不利于维护。

经过两个单元的学习,能明显感觉到设计能力的上升,但依然长路漫漫,有很多东西需要进一步地学习。

posted @ 2021-04-26 19:44  Jareth  阅读(198)  评论(0)    收藏  举报