301
社区成员
发帖
与我相关
我的任务
分享这一部分主要介绍了电梯调度各次作业中类和线程协作的设计与迭代,概括了多线程程序的整体框架,包含UML图和UML协作图。
1.线程和类的总体架构 根据题目背景,结合可扩展性,我采用了生产者-消费者模型来构建电梯调度程序;并且以输入线程-调度线程-电梯线程作为三个节点形成两级流水线结构,其中输入-调度以输入队列InputQueue为共享托盘;调度-某电梯以电梯门口的队列ElevatorQueue作为共享托盘,为了方便电梯取用,后者还做了分楼层处理。该模式示意图如下。

2.类的设计和迭代
在首次(第5次)作业中类的设计的要点有:
分离电梯类和电梯调度策略类分离。具体来说,将电梯建模成状态机模型,具有WAIT,OPENED,EXITED,WORKED,ENTERD,CLOSED,MOVING等状态,实现了up,down,open,close,work,enter,exit等状态转移过程;策略通过将Operation类的对象交给Elevator通知后者下一步的行动。这样做的好处是电梯能够相对而言比较简单,调整策略时电梯类的修改较小,符合SRP;但有比较明显的缺点,即在我的实现中,存在大量的私有数据交换。

另一方面,由于各个队列比较相似但不完全一致,因此采用了我继承的方式构建了各个队列。RequestQueue实现了add,wait,setEnd等方法,再由各个子类实现它们相应的方法。

第二次作业(第6次) 提出了全局调度的要求和重置请求。

第三次作业(第7次) 引入了双轿厢电梯。
双轿厢电梯的需求在类设计的主要难点在于双轿厢电梯的地位问题,即将其看作两个独立电梯还是看作一个电梯的两个部分。为了进行高效的协作,我将其建模成两个独立的电梯。在进行电梯调度时,我将双轿厢电梯视为与单轿厢电梯地位相同的电梯进行分配。不过由于之前的作业中电梯是以运行在全楼层的单电梯的形式对外提供接口的,因此为了减少修改量,我新增了电梯井道ElevatorWell类封装单轿厢或两个双轿厢电梯对外提供统一接口。双轿厢电梯有一些不同于单轿厢电梯的特性,比如楼层有范围限制,输出不同等。然而绝大部分属性和方法同单轿厢电梯完全一致,因此我引入DoubleCarElevator类作为Elevator的子类对原有功能进行了扩展
另一方面,第六次作业中直接将电梯当作共享对象来使用的缺陷进一步凸显,许多地方的同步和互斥逻辑难以理清,针对这个问题我进行了一些更改,把电梯中大部分锁去除,改成使用共享对象实现电梯与调度器之间的通信,如新增了共享对象ElevatorResettingSignals,NumberDirSharing;同时新增另一个共享对象ChangeFloorOccupied实现双轿厢电梯对换成楼层的互斥进入。此次作业的整体架构如下。

最后补充在迭代设计中的一个小问题。在之前的设计中,由于对未来的需求并不能准确预测,因此一些类的设计并不是十分完善,但在明显有限的迭代过程中,也没有必要为了设计的美观直接重构,可以在原来的基础上通过继承的方式添加新的功能。具体来说,原先我设计了Output类负责输出,但是后来新增了RECEIVE输出,又新增了双轿厢电梯的输出,因此我分别新建了ReceiveOutput和OuputDouble继承原有Output,扩展出新的功能。这样设计虽然不是特别美观,但是减小了工作量,效率比较高;同时也是OCP的践行。
可扩展性和可改进点分析。在最终设计的基础上,程序还有没有进一步扩展的能力呢?

```
protected void changeState() {
Operation operation = strategy.getNextState();
switch (operation.getBehavior()) {
case UP:
up();
break;
case DOWN:
down();
break;
case OPEN:
open();
break;
case WORK:
work();
break;
case CLOSE:
close();
break;
case IN:
enter(operation.getPassengerSet());
break;
case OUT:
exit(operation.getPassengerSet());
break;
case ALL_EXIT:
exitAll();
break;
case WAIT:
waitForClients();
break;
case NORMAL_RESET:
normalReset(((ResetOperation)operation).getNormalResetRequest());
break;
case DOUBLE_CAR_RESET:
doubleCarSet(((ResetOperation)operation).getDoubleCarResetRequest());
break;
default:
throw new RuntimeException("Unknown Operation of elevators");
}
}
```
3.线程协作关系
下面以UML协作图(时序图)的形式分析最终设计中线程之间的协作关系。这一部分做简单展示,具体的实现在第二部分“调度器设计和调度策略”和第三部分“同步块和锁的设计”详细介绍。
主线程负责开启和维护各个线程。

输入线程将乘客请求放入输入队列,将重置请求直接转发给电梯线程。

调度器前和输出线程协作,后和电梯线程协作;分别通过托盘Queue和若干信号共享对象(ElevatorResettingSignals/ElevatorWaitingSignals/NumDirSharing)实现。

电梯线程和输入线程存在协作,作为消费者接收reset请求;和调度器有协作,作为消费者接受乘客,同时作为生产者提供部分电梯状态。

4.设计稳定性分析
这个部分结合我的两级流水线架构,介绍了单个电梯在调度门口队列的调度策略以及负责将请求分配给各个电梯的总调度器的设计和策略。
这个部分结合整体架构和调度策略的数据共享需求,介绍了线程之间的同步和互斥机制在方法和同步块级别上的具体实现,包含第三次作业中防止两个电梯相撞的方法。
1.测评机搭建 本人实现了简单的评测机器,支持进行随机和有一定指向的输入的生成;能够基于约束对输出进行检查。生成的思路是采用面向对象的方式,将乘客/reset请求建模为类,各个属性如楼层/电梯可用随机数进行生成,有一部分可以方便的手动设置。进行测试的思路是采用面向对象编程,模拟电梯运行。先从输入中解析乘客信息,维护初始电梯信息。过程中针对每一项输出,对电梯运行的楼层/开关状态/载客状态/reset状态/时间,以及乘客的动作进行合理性分析。例如对arrive。
def arrive(self, floor, time):
if floor == self.cur_floor:
raise CustomError(f"还没动呢怎么就到了 At{self.cur_floor} id:{self.elevator_id}")
if abs(floor - self.cur_floor) > 1:
raise CustomError(f"跑了多层 From{self.cur_floor} To{floor} id:{self.elevator_id}")
if floor < MIN_FLOOR or floor > MAX_FLOOR:
raise CustomError(f"幽灵楼层 From{self.cur_floor} To {floor} id:{self.elevator_id}")
if not (time - self.last_behavior_time >= self.move_time + EPSILON):
raise CustomError(f"超速 From{self.cur_floor} To {floor} id:{self.elevator_id}")
if self.state != ElevatorState.PARKING:
raise CustomError(f"没关门 Elevator:{self.elevator_id} From{self.cur_floor} To {floor}")
if len(self.passenger_list) == 0:
if not isinstance(self, DCElevator) or self.cur_floor != self.change_floor:
raise CustomError(f"没有receive就动 id:{self.elevator_id}")
if self.state == ElevatorState.RESETTING:
raise CustomError(f"resetting时候乱动 id:{self.elevator_id}")
if self.reset_ready:
self.arrive_count += 1
if self.arrive_count > 2:
raise CustomError(f"未及时响应reset id:{self.elevator_id}")
self.cur_floor = floor
完整代码见https://github.com/zenghxSSS/BUAA-OO-Elevators-judge
2.死锁的分析(RTLE/WA) 针对可能出现的死锁问题,我的方法是追踪全体synchronized方法,查看外部是否存在其他同步块,然后将加锁的对象建立成一棵树,如果出现了环,则说明在一定条件下会出现死锁。我使用了该方法找到了上述死锁。如果无环,可以确定不存在死锁问题。
3.wait-notifyAll模式下的轮询(CTLE)/线程无法停止(RTLE)/线程提前停止问题(WA)这几个问题我也全部出现过。如果出现轮询(必要时候没有wait),可以在每个循环内部使用print大法进行分析,直到找到出现极大量输出的情况;修复只需在对应位置加好wait-notify即可。对于线程无法终止问题大概是由于同步控制不好,notify信号丢失或者notify信号没有加够导致的,比较难以复现,如果没有指向明确的数据,我几乎没有复现出来过,即使采用密集数据轰击也很难找到,这可能是个难题;修复也比较困难,往往需要另外加同步块进行wait-notify,而这又可能会导致死锁,因此建议新建一个对象作为共享对象进行控制。线程提前终止的问题往往是前一个问题的反面,复现和修复较为困难,可能的方法和上面相似。
4.调度策略导致的CTLE分析 我在第二次作业出现了许多同学都出现的负载不均衡问题。当许多电梯都在reset的时候就会将瞬间到来的大量请求全部给到剩下的电梯,于是reset回来的电梯就空闲等待,原来的电梯超负荷运转,最后超时。解决方法适当等待即可。
本单元的设计中我有许多体会,也仍然有很多困惑,由于时间实在是来不及了,简单写两条。