BUAA_OO第二单元总结

第二单元作业总结


一、整体架构设计

(注:输出类仅仅是对原有输出方法进行了封装,以下未单独列出。)

1.1 第一次作业

本次作业参考了实验课上的架构:

  • 主线程main设置初始变量、创建类、启动线程。
  • 输入线程input专门处理输入请求,并将该请求传入主调度类。
  • 主调度线程schedule处理来自输入线程的请求。在本次作业中,它的职责主要是将请求加入相应电梯的待乘队列。
  • 电梯线程elevator模拟电梯的运行。需要指出的是,在我的架构中,电梯仅仅只有运行的功能,它不断地上下行、停靠、上下客,但所有这些任务的执行都由电梯调度器控制。
  • 电梯调度器类arrange用于调度电梯的运行。每一部电梯都内置一个单独的调度器,每个调度器都拥有一个待乘队列和一个乘客队列。 调度器通过这两个队列,控制电梯的上下行。
  • 请求队列类requestqueue,这是一个线程安全的队列,用于存放乘客请求。上述各种队列指的都是该类。

本次作业的类图如下图所示。

1.2 第二次作业

本次作业仅仅加入了横向电梯,且无换乘需求。由于纵向电梯与横向电梯是相互隔离的,纵向电梯本质上也是一种横向电梯,它们具有的属性几乎完全相同,仅仅在调度算法上有所差别。因此,可设立电梯接口与电梯调度器类,纵横向电梯及调度器可实现或继承它们。
具体的架构与第一次作业相似,为:

  • 主线程main职责不变。
  • 输入线程input专门处理输入请求,当请求为新增电梯请求时,立即初始化相关变量并启动新的电梯线程。当请求为乘客请求时,将该请求传入主调度类。
  • 主调度线程schedule处理来自输入线程的请求。在本次作业中,它的职责依然是将请求加入相应电梯的待乘队列。具体如何加入详见后文。
  • 电梯线程elevatorB、elevatorF模拟电梯的运行。它们实现了电梯接口。前者是纵向电梯,后者是横向电梯。
  • 电梯调度器类arrangeB、arrangeF用于调度电梯的运行。前者控制上下行,后者控制左右行。
  • 请求队列类requestqueue,没有改动。

本次作业的类图如下图所示。

1.3 第三次作业

本次作业新增了换乘需求,但架构没有太大的改变,仍与前两次作业类似。为了支持换乘,新增了乘客类person,它是一种对personrequest的封装。这次作业最大的改动发生在主调度器线程schedule,它不再像前两次作业一样机械地将乘客放入相应的电梯中,而是具有了更多调度功能。
具体的架构为:

  • 主线程main与第二次作业基本相同。
  • 输入线程input与第二次作业基本相同。
  • 主调度线程schedule处理来自输入线程的请求。当乘客需要换乘时,它将乘客的请求静态拆分。之后,它从乘客所在的楼座/楼层中选择最优的电梯,将乘客放入该电梯的待乘队列。
  • 电梯线程elevatorB、elevatorF模拟电梯的运行。与第二次作业基本相同,新增了若干属性。
  • 电梯调度器类arrangeB、arrangeF用于调度电梯的运行。与第二次作业基本相同,新增了与换乘有关的内容。
  • 请求队列类requestqueue,新增了与换乘有关的内容。
  • 乘客类person,封装了乘客请求类,为换乘提供支持。

本次作业的类图如下图所示。


二、调度策略

2.1 第一次作业

本次作业中,只有纵向电梯、每栋楼只有一部电梯,故只需考虑电梯内部的调度策略。
电梯整体采用look调度策略,具体的流程为:

  • 电梯内没有乘客,主请求已送达,则从待乘队列中选择一项作为主请求

  • 电梯内有乘客,主请求已送达,则从乘客队列中选择一项作为主请求

  • 其它情况,每到一层都会遍历待乘队列和乘客队列,若其中有一项请求满足:

    • 该请求与主请求同向,若是待乘请求,还需要要求电梯未超过出发楼层
    • 该请求的目的层与电梯目前所在层的距离大于现有请求

    则将该请求设为主请求

  • 捎带条件:

    • 不会超载
    • 被捎带请求与电梯运行方向同向

以上便是调度策略的大致内容。

2.2 第二次作业

2.2.1 电梯调度策略

本次作业,纵向电梯的调度策略与第一次作业完全相同。
横向电梯调度策略:

  • 电梯内没有乘客,主请求已送达,则从待乘队列中选择一项作为主请求
  • 电梯内有乘客,主请求已送达,则从乘客队列中选择一项作为主请求
  • 捎带条件:
    • 不会超载

可以发现,横向电梯的调度策略非常简单,没有考虑运行的方向问题。这是因为,电梯会根据主请求的目的地自动决定运行方向。

2.2.2 电梯选择策略

由于同一楼层/楼座可能有多部电梯,故需要选择将乘客放入至哪一部。我没有采用自由竞争的方式,而是直接静态决定,且方法和基准策略一致:依次放入。即这次放入第一部、下一次放入第二步,以此类推。

2.3 第三次作业

2.3.1 电梯调度策略

本次作业中,电梯内部的调度策略没有改变,与第二次作业基本一致。

2.3.2 换乘策略

以下均假设乘客必须换乘。

  • 第一步:寻找换乘楼层。方法与基准策略一致。
  • 第二步:如果换乘楼层与乘客出发楼层不在同一层,调整乘客的请求,包括:目的楼座和目的楼层。经过此步骤后,乘客的每一阶段请求都不再具有换乘属性,那么在接下来的步骤中,它将被视为一个普通请求。
  • 第三步:乘客进入电梯,到达第一阶段目的地。
  • 第四步:刷新乘客请求至第二阶段。将乘客放入换乘队列。
  • 第五步:主控制线程重新接受该请求,但此时将其视为普通请求即可。
  • 第六步:乘客继续进入电梯,重复上述流程,直到最终到达。

2.3.3 电梯选择策略

2.3.3.1 纵向电梯选择策略
  • 对于每一部电梯,估测等待时间。注意,此处只是一种十分粗糙的估计,它只是通过乘客的起始位置和电梯的起始位置估测运行时间。选择一个最短时间的电梯,将乘客放入它的待乘队列。
  • 没有考虑载客量等属性。
2.3.3.2 横向电梯选择策略
  • 每一部横向电梯在创建时便具有一个初始属性,它由运行速度、载客量和可达楼座综合决定。
  • 当电梯满员时,另外设置属性。
  • 综合考虑,选择评分最大的电梯。

三、同步块与锁

由于三次作业的设计是迭代的,故只以第三次作业为例,介绍同步块和锁的设置。

第三次作业中,使用锁的地方为:

  • 请求队列类requestqueue的所有方法均上锁。
  • 输入线程input,在添加电梯时,对电梯的待乘队列hashmap及电梯池hashmap上锁。这两个变量与主调度线程共享。
  • 主调度线程schedule,在遍历同一楼座/层的电梯时,以及将乘客放入相应的待乘队列时,对共享变量上锁。
  • 电梯调度类arrange,在遍历待乘队列时对其上锁。
  • 输出队列output,对输出方法上锁。

同步块基本上分为以下几类:

  • 需要对共享变量进行遍历(这在调度及控制时常见)
  • 需要对共享变量进行修改(这在转移乘客请求时常见)

waitnotifyall的设计

所有的阻塞与唤醒均只在请求队列内出现。仅在获取请求时可能阻塞,在放入请求或结束时会唤醒。


四、sequence diagram

4.1 第一次及第二次作业

时序图如下图所示,为了使图更简洁,省略了循环部分的说明。

  • 主线程创建其它线程
  • 输入线程处理输入,并将其传递给主控制类
  • 主控制类根据接受到的请求,为乘客分配电梯
  • 电梯每到达一层,都会询问调度器接下来该去哪一层
  • 调度器进行调度,告诉电梯该去哪里
  • 电梯根据指示运行
  • 随后,电梯检查该层是否有上下客需求
  • 完成任务,线程结束

4.2 第三次作业

时序图如下图所示。

  • 主线程创建其它线程
  • 输入线程处理输入,若是电梯请求,则新建新电梯;若是乘客请求,则将其传递给主控制类
  • 主控制类根据接受到的请求,静态拆分乘客请求,设置换成楼座,为其分配电梯
  • 电梯每到达一层,都会询问调度器接下来该去哪一层
  • 调度器进行调度,告诉电梯该去哪里
  • 电梯根据指示运行
  • 随后,电梯检查该层是否有上下客需求
  • 出电梯的乘客若未到达目的地,转入换乘队列
  • 主控制类接受换乘请求,重新为其分配电梯
  • 重复运行,直到乘客最终到达目的地
  • 完成任务,线程结束(具体结束逻辑详见下文))

五、个人bug分析

在这三次作业中,我出现了若干处bug,有些是在本地测试时发现的,有些是在互测时发现的,例如:

  • 输出线程安全问题
    • 虽然单独封装了输出类,但最初时没有将其设为静态方法,导致时间戳仍然不能递增输出
  • 结束逻辑
    • 什么时候线程应该结束?尤其是在第三次作业中,由于支持换乘,即使电梯内部的待乘队列和乘客队列都为空,且输入已经结束,都不能认为电梯线程应该结束,因为很有可能之后换乘的乘客会乘坐该电梯
    • 为此,我引入了信号量,每当处理一个初始的乘客请求时,该变量自增。当乘客最终到达目的地时,该值自减。因此,各线程的结束逻辑为:
    • 输入结束,则输入线程结束。
    • 输入线程结束、输入队列为空、所有乘客都到达,则主调度线程结束。在结束前,它会通知各调度器。
    • 主调度线程通知调度器、且电梯的待乘队列和乘客队列都为空,则电梯线程可以结束。
    • 所有其它线程结束后,主线程结束。

六、他人bug分析

    十分遗憾地是,在这三次作业中,我未能发现同学的 bug。
    而且我所在的房间也非常平静。可能是大家都爱好和平hhh

6.1 测试策略

  • 手动构造特殊数据
    • 自己在完成作业时,很可能会遗漏了某些特殊情况,例如超载、同一层反复上下客,以及一些有意构造的可能会超时或造成死锁的数据,这些错误有可能也会发生在他人身上,因此可以试探性地提交这些测试数据
  • 随机生成大量数据
    • 通过随机构造数据,检验正确性
    • 随机的“绝对性”:乘客的起始位置是随机生成的
    • 随机的“相对性”:可以根据一些特定的需求,让生成的随机数据具有某种特性。例如,所有乘客都在同一层上/下、在某一个时间点生成大量数据等

6.2 线程安全

  • 人工检查
    • 逐行分析同步块即锁的使用,检查是否满足死锁的必要条件
  • 测试检查
    • 大量运行随机数据,运行次数越多,发现问题的概率越大

6.3 测试策略的差异之处

  • 本单元作业与第一次作业测试的最大差异在于:第一单元作业是静态、可复现的,本单元是动态、不可复现的。 由于引入了多线程,同一组测试数据,多次运行很可能有不同的结果,因此需要进行大量的测试。
  • 正确性的判断更为自由。受调度策略的影响,每个人的电梯运行模式都有可能不同。本单元的正确性,实则是一种“合理性”。因此,在判断条件上更为灵活。

七、心得体会

7.1 线程安全

本单元作业,线程安全可谓最重要的一点。初步接触多线程设计,最初的架构很有可能是线程不安全的,因此需要经过仔细、漫长的设计、思考、检查。线程安全问题主要出现在以下几方面:

  • 死锁。最主要的问题。为了避免死锁,需要花费大量的时间检查、测试。
  • 同步与互斥。哪些地方需要对共享变量进行访问?哪些代码位于临界区中?如何尽量缩小临界区?
  • wait与notifyall。哪些地方需要设置wait?哪些地方需要使用noyifyall?

7.2 层次化设计

本次作业,架构设计十分重要。何时何处处理乘客请求?调度器放在何处?如何支持换乘?这些问题的答案十分不容易得到。
幸运的是,第一次作业中,从实验课代码得到启发,初步建立了行之有效的架构。最初,我坚持把调度器作为电梯内部的一个属性,以便支持电梯的个性化调度,但没有对主调度类进行良好的设计,导致性能优化非常困难。第三次作业中,我在不改变原有架构的基础上,调整了层次关系,凸显了主调度类的重要性。

posted @ 2022-04-27 22:56  DreamWave  阅读(28)  评论(0编辑  收藏  举报