第二单元实验总结 | TrickEye

第二单元实验总结 | TrickEye

基本情况部分

  • 这篇帖子为什么会在这?
    • 这是北航计算机学院面向对象构造与设计2022春季课程第二单元的总结博客
  • 本次作业的要求是什么?
    • 模拟一种强制在线的目的选层电梯的调度,有必要优化其性能
    • 电梯支持横向运行,支持热添加,支持自定义可达性

架构篇:个人代码结构分析

下面将逐类分析设计这个类的目的和必要性。

a. BuildingElev建筑(纵向)电梯

  1. 这个类通过继承Thread类,被视作一个线程
  2. 作为一个电梯,这个电梯首先应该有舱内人数curCapacity,现在所在层curFloor,移动时间transferTime,是否开门doorOpen等等基本属性,应该实现开门open,关门close,上行moveUp,下行moveDown,载人board(PersonRequest),卸载unboard(PersonRequest)方法
  3. 一个电梯应当有一个直接服务的队列toserve,一个自己维护的,被视为舱内的队列passenger,这两个属性我是通过自己封装了泛型类Queue<>来维护的。在之后会提到。
  4. 一个线程应当有run方法。这个run方法的主要内容即为:检查当前的所有条件,除非满足电梯结束服务的终态条件(这将杀掉这个线程)电梯将会检查自己的相关服务对象,通过某种策略在(2)中列举的诸多基本操作中选择一种。、
  5. 因此需要设计各种策略,或者Strategy,以及他们的工具方法。本人在开发过程中使用的是MyStrategy(),性能问题,最终诉诸于lookStrategy()

b. FloorElev楼层(横向)电梯

  1. 这个类也是一个线程
  2. 横向电梯与纵向电梯相比,使用了递增式移动moveInc(),递减式移动moveDec()
  3. BuildingElev
  4. BuildingElev
  5. 策略方面,我的策略和look策略对强测的性能没有过于重大的影响,因此Bug修复环节也没有更改这部分的内容
  6. 关于为什么FloorElevBuildingElev没有继承自一个公用父类或者是一个共同接口的不同实现,这是因为课程组的CheckStyle规则不允许我使用protected关键字,因此若继承自同一类,则公用的方法不能提取到父类,若作为同一接口的实现,则共用变量不能被提取到接口中。我其实也不是很清楚为什么CheckStyle为什么不让用protected,但是为了避免节外生枝,也就即使有不少重合部分,也写了两个类。在之后的部分会有这方面的讨论。

c. Queue<E> 泛型队列类

  1. 这个类封装了一个ArrayList,其具体的类型推迟到在创建时才被确定,因此是泛型类。
  2. 这个类主要是为了实现队列作为一个多线程共享对象时,需要通过isEnd信号来为服务此队列的线程传递是否可以终止的信息。
  3. 此外就是涉及到一个数据结构的增删访问了。此外,自己写的这个泛型类只是对于ArrayList这种更强的泛型数据结构的一次拙劣的模仿,迭代器是不能直接作用于其上的,同时也涉及到深浅拷贝的问题,因此也提供了asList()方法,把所有的元素拿出来返回一个新的表(相当于深拷贝)

d. MyPersonRequest 复杂请求类

  1. 这个类继承了PersonRequest类,用来抽象一个涉及到换乘的请求。

  2. 一个这种请求在被创建时应当首先由某种策略来决定换乘楼层,指定换成楼层后这个请求被分为三个阶段,在每阶段结束之后将会改变自身的状态,并重新填回相应的队列中进行接下来的调度。从外部来观察,也就是getFromFloor, getToFloor, getFromBuilding, getToBuilding出现变化

e. InputHandler 输入处理线程

  1. 这个类是一个线程
  2. 他将读取每一个输入请求,放入对应地队列,交由对应的应答线程来处理

f. PrimarySchduler 调度器线程

  1. 这个类是一个线程
  2. 从上游的队列中取请求,放到对应的楼层或者楼栋的队列中。其实我使用的类似于自由竞争的算法,在总的调度器这方面并没有太复杂的设计

架构篇:设计模式的使用和思考

首先,生产者消费者模式在此次作业中的地位是具有支配性的。整个调度的过程其实就是由输入处理线程“生产”请求并置入一个共享容器,再由下游的电梯线程来从共享容器中取走这些请求,完成调度。但是实际上此次作业也不完全是纯粹的生产者消费者模式,在一些细节上需要做出一些适当的改变以符合题目的要求,具体表现为以下几点

  • 共享对象的消费顺序不同于生产顺序
    • 就笔者看到的生产者消费者模型的例子中,共享对象(或者共享的容器)往往是容量为1,或者是FIFO表。这样做好处在于实现起来比较简单,但是并不一定适合我们的电梯。毕竟请求不应该是按照某一种顺序来的,电梯在决定服务哪个请求的时候也不应当用请求被插入队列的时间来作为判据。
    • 那维护某种优先队列好不好呢?固然好,但是这种优先队列的排序依据是否会根据电梯所在的位置不同而产生改变呢?例如,同样是分别从9到10和从2到1的请求,电梯在3楼的时候就应该先服务后者,在10楼的时候就应该先服务前者。
    • 因此不同于传统的生产者消费者模式,电梯调度问题不是一个共享对象里面的removeRequest()就能解决问题的,应当把所有的请求,以及电梯的一些信息放在一起考虑,这就是调度的问题了,不过不可否认的是,这会对我们的架构设计产生一些困难和挑战
  • 消费者不唯一
    • 消费者不唯一是自由竞争容易出现的问题。多个电梯如果服务同一个队列就有可能出现多个电梯同时决定服务某一个请求的问题。如果不在线程安全性上加以思考和研究就有可能出现不合理的输出。
    • 为了试图解决这个问题,我使用了一种所谓“锁定”的机制,当一个电梯决定服务一个请求的时候将会把这个请求从服务的共享队列中取出,并置于一个私有的锁定区域内,别的电梯不可见。这样做要求我对这个锁定区域与电梯的服务队列和乘客队列之间的交互逻辑做好设计,例如在什么时候从哪个地方取出,并放到什么地方,放到的这个地方原有的东西又怎么处理。关于这一部分的逻辑,我会在调度篇讨论
    • 当然有的(大多数)同学也没有使用我的方法,他们的想法是,只要保护好对象不被多个电梯抢来抢去,不出现答案错误,多个电梯奔着一个人去也是合理的,因为可能后续的请求也会从那附近发出,如果如此那么另一个电梯恰好从一个更近的地方出发,也就是一件好事。

  • 存在下游队列反哺上游队列的情况
    • 在第三次作业中,我使用了一种将请求通过MyPersonRequest类封装、拆分成几个阶段。当一阶段的请求完成时将会重新加回请求队列并重新分配。
    • 这样做使得决定某一个队列是否结束的因素既取决于上层队列(输入是否停止),也取决于下层队列是否有反哺的请求。如果不想清楚这个逻辑,就很容易产生线程难以停止的问题。
    • 在我使用的逻辑中,让一个队列停止的逻辑为:如果上游队列结束,且上游队列为空,则此队列为空。对于最上游队列,当输入结束,并且没有需要反哺的请求时队列结束。

架构篇:协作图

调度篇:电梯自调度-ALS与LOOK

这一部分主要是当一个电梯面对要服务的请求集合的时候,决定其行为的算法。

课程组使用了一种可稍带的ALS策略,这种策略是按照被加入队列的先后顺序来分配电梯服务的顺序,同时根据一种合理的逻辑确定是否捎带。

广大同学们使用的是LOOK算法,这里我不在讲述。

而我在思考的是,对于这种电梯自调度的问题,找得到一种最好的调度策略吗?找不到。每一个调度策略是不是都有一些数据可以卡住它呢?完全可以吧。LOOK算法是最好的算法吗?我曾经不这样认为,但是经过了电梯月,经过了多次强测,我不得不承认综合考虑实现的复杂程度和算法性能,LOOK确实比其他的算法有一定的优越性。

只可惜当时的我不愿意所谓的随大流,总是试图从LOOK之外找到一种自己的调度算法,结果付出了极其惨痛的代价。我仿照ALS,但是是以距离作为排序的依据,但是仍然被一些数据点限制,退化成了一些很弱的算法。

调度篇:多电梯协作-自由竞争与预先分配

当有多个电梯服务职能相同时,有的同学使用的是预先分配,即由调度器采用某种算法将应当由这两个电梯共同服务的队列分别放到这两个电梯各自需要服务的私有队列中,然后这两个电梯就可以通过之前实现的电梯自调度算法来服务这些请求。这样做的优点是完全不需要对电梯自调度的算法做任何更改,但是不合理的预分配会浪费一些效率。

有的同学使用的是自由竞争方案,也就是两个电梯服务一个队列,做好线程共享对象的保护以确保不出现逻辑错误,然后两个电梯同时服务,在自由竞争中完成服务请求。这样做的好处是可以尽可能地平均多个电梯之间的负载,但是不好的地方在于实现需要一些更改,同时,也会产生多个电梯追一个人的情况(这倒不一定是好事,也不一定是坏事)

我通过引入了一个锁定区,在实现自由竞争的同时解决了多个电梯追一个人的情况(但是已经提到过了,这不一定是一件好事,也不一定是一件坏事)具体逻辑为:一个电梯在任何时候都应当有一个唯一的主要服务目标,当电梯决定把一个共用队列中要服务的请求视作主要服务目标的时候,会将其从共用队列中移出并置入锁定区。

也是在这里我体会到了线程不安全带来的严重后果。

调度篇:换乘安排-预先安排和动态安排

我采用的方法是将一个需要换成的请求先通过可达性和距离计算指定换乘楼层,然后把这个请求拆成三段。这样做的好处是比较好实现,但是也还是有可能出现在一些楼层请求堆积的情况。

似乎有的同学也采用了最短路的算法(不过我觉得投入转化成产出有点少,就没有做)

测试篇:强测与互测

第二单元我的测试并不是很理想。我也非常的懊悔。

似乎击破我的大多都是因为不合理的调度产生的强测RTLE错误,也有一些线程不安全的情况。

例如,第二次作业的所谓锁定区产生了一些线程不安全的问题,导致同一个人进了两个电梯;第三次作业种植条件设计的不合理导致线程停不下来。

也找到了一些同学的错误,例如环形电梯的死循环,同层但是不可达的请求没有换乘。

感想

电梯问题似乎是北航OO的专属问题了。电梯月之前我在各大搜索引擎搜索电梯调度策略,全是往届学长学姐的博客。减掉博客园的关键字后就只剩电梯厂家的广告了。

但是现在而言似乎所有的调度都成为了LOOK一家独大的战场,我不喜欢这样拘泥于LOOK算法,不喜欢追随学长学姐的脚步,于是在LOOK算法之外试图创造自己的调度策略来对比不同调度的好坏。但是课程组的要求是我的调度不能比一个算法慢太多。因此太慢的点就会被视为错误。

实际上我的代码也不算是太坏吧。在一些点也拿过99分,但是课程组这样的评价:快,拿性能分;慢,整个点挂掉就会让我付出一些代价。我承认我的调度确实在一些地方有不合理之处,但是我的确可以保证我的代码的正确性,这样看来,我至少应当与那些线程不安全的错误有一点差距吧。


关于本题的要求,我有一点有一些看不惯,凭什么是到达楼层的时候输出呢?我们班的讨论课甚至出现了所谓量子电梯:先等待,再输出还是关门。我们学的是OO,这种编程思想是来自人类自古以来就有的实践得到的经验,我们写的代码也理应是解决生活中的实际问题的,这种量子电梯什么的完全不符合生活实际吧!而且课程组完全可以通过先输出再sleep来规避这种现象吧,总之我不喜欢这个小细节。


另外,多线程行为诡异,难写,难调,难测。这个单元掉了不少头发,但是还是没有得到一个好的结果。希望之后的内容能够对我好一点。

posted @ 2022-05-04 13:23  TrickEye  阅读(44)  评论(0编辑  收藏  举报