BUAA-OO-UNIT2 多线程电梯调度

BUAA-OO-UNIT2 多线程电梯调度

作业背景

在北航的面向对象课程第二单元作业中,我们需要在三周内的三次作业中分别完成下述任务:
第五次作业:模拟一个简单的多线程实时电梯系统。
第六次作业:在第一次作业的基础上增加模拟电梯系统扩建和日常维护时乘客的调度功能。
第七次作业:在前两次作业的基础上对电梯的可达性、同一楼层的服务中和只接人电梯数量进行了限制,可能出现乘客换乘现象。
得到的分数取决于正确性(85%)与性能分(15%);性能分取决于运行时间、请求等待时间和电梯耗电量

总体设计

线程设计

我采用了两种线程的方式进行设计。

  • 输入线程(InputHandler)作为生产者,从终端不断地读取请求;

  • 初始的和增添的电梯线程(Elevator)作为消费者,接受请求并不断地进行状态更新。

对于每个电梯线程,我都给予一个托盘,即一个乘客请求表(RequestTable)。在输入线程中,如果读取的请求是乘客请求,那么就按照设计的分配策略(Dispatcher),直接计算出这个请求在此时此刻该被分配到哪个电梯线程中,并直接进行分配。因此我并没有大家可能会有的调度器线程,而是将调度器设计成了一种方法类,用特有的计算方法将输入线程和电梯线程进行交互。

调度设计

在输入线程将请求分配给每个电梯线程后,对于每个电梯线程都只需要对自己的请求表进行电梯调度,而不需要考虑其他因素。对于每个电梯线程,我采用的调度算法为LOOK,这个算法效率较高,也符合现实生活中我们坐电梯的认知。

具体来说,我对每个电梯线程采用状态模式建模,设置了状态的五种可能性(初始的Rest, Move, Open, End以及第一次迭代后的Maintain),放在了枚举类Status中;每个电梯线程在运行时都会时刻对于当前楼层使用LOOK算法进行评估,并进行状态的切换。

LOOK算法也很简单,即电梯在每次抵达新一楼层时都对当前楼层的候程表及此时的乘客进行评估。

  • 若此时当前楼层的候程表有人,并且他的运动方向和电梯此时的运行方向一致,或者此时乘客有已经抵达目的楼层需要出去的,那么就开门,进行电梯与外界的乘客交换;

  • 否则,如果乘客不为空,或者乘客为空,并且在电梯的当前方向前方有乘客需要进入电梯,电梯就继续沿当前方向行驶;

  • 如果乘客为空,并且在电梯的当前方向上没有乘客,但在后方有乘客需要上电梯,或者当前楼层有乘客但他的方向与电梯的运行方向相反,那么电梯改变方向。

通过LOOK算法,就可以尽量避免有乘客等待时间过长的情况。

同步块与锁

本次作业由于是多线程问题,会出现线程安全的相关问题,因此需要对多个线程对共享对象进行读写的情况进行同步控制,告诉JVM要对这个语句块进行线程互斥控制,采用的方法就是上锁。虽然课程组介绍了许多不同的锁,如ReentrantLock, ReentrantReadWriteLock等,但我的设计并不没有使用其他特殊的锁,只使用synchronized实现对同步块的控制,其他的锁在我的设计中并不是必要的。

具体来说,需要在多个线程可能会同时读写一个共享对象的情况的地方为方法或者共享对象加synchronized关键字,防止一个线程在访问共享对象时,另一个线程对其进行修改导致出现错误。在我的设计中,共享对象即为每个电梯的候乘表,因为在电梯访问自己的候程表时,可能会出现两种情况:

  • 输入线程读取新的乘客请求,并将其分配给该电梯的候程表,导致读写冲突。
  • 其他电梯线程接受maintain指令后将自己的乘客分配给该电梯的候程表,导致读写冲突。

因此需要在每一个访问电梯候程表的方法中添加synchronized关键字,在当前线程访问电梯候程表时阻塞其他线程对候程表的修改。

此外,在第七次作业中,课程组还介绍了semaphore信号量,这个锁被我用来控制同一楼层服务中和只进人的数量,它能够实现比锁更强大的线程同步控制功能,并且是线程安全的。

难点分析和解决方案

第五次作业

最难的无疑是从0到1的多线程设计,需要实现线程同步,并且对电梯调度进行设计。需要对整体架构进行清晰的构思,并且猜测未来的迭代方向。

我的乘客分配方法设计的非常简单,就是模6法,每个乘客按照循环顺序分配给六部电梯。自然是有运行时间上的弊端,但在这次作业中考虑因素没有那么多的情况下,运行情况也还可以。相对于自由竞争,耗电量也较小。

第六次作业

有两个难点:

  • 添加的电梯参数与其他初始电梯并不完全相似,如果仍采用傻瓜分配方法,可能会导致一个很垃圾的电梯(譬如最大承载量为3,速度为0.6)承载与其他性能较好的电梯一样多的乘客,这可能会导致总体运行时间过长。因此需要好好思考如何设计分配策略。我采取的方法是通过速度speed,最大承载量maxLoad,当前乘客数currentLoad和候程表内的总请求数requestNum四个参数,计算电梯的适应度fitness,计算公式为
    f i t n e s s = l n ( m a x l o a d s p e e d ) ∗ e − c u r r e n t L o a d − r e q u e s t N u m fitness = ln(\frac{maxload}{speed})*e^{-currentLoad-requestNum} fitness=ln(speedmaxload)ecurrentLoadrequestNum
    看上去很高端,其实就是针对四个参数分别满足各自的单调性就可以了lol,这样计算出的适应度一定是正的,并且适应度越高,在分配乘客时的优先级越高。这样可以根据四个参数来计算电梯是否适合接需要分配的乘客,能够在一定程度上节省总体运行时间。

  • 维修的电梯在退出电梯系统时,如何将其中的乘客和候程表内的请求分配给其他电梯。我采用的方法是如果一个电梯需要维修,就将其适应度置为0,并将其中的乘客和候程表内的请求的起始楼层设为当前楼层(如果没有抵达当前楼层的话),并将他们都重新进行分配,这样就一定能避开当前维修的电梯。

第七次作业

有三个难点:

  • 如果将所有可达性为0x7ff的电梯均维修掉,那么有可能会出现乘客必须经过换乘电梯才能抵达目的地的情况。

    我采用的办法是根据每部电梯的可达性画一个电梯系统的图,用二维数组表示,图中第(i, j)位的值为从第i层楼到第j层楼有几部电梯可以直达。初始化图为一个11*11,每个空的值均为6(因为初始化了六部可达性为0x7ff的电梯)的二维数组,对于每次新加的电梯和维修的电梯,均根据其可达性将图中对应位置的值进行加减。

    对于每个乘客,根据他的fromFloor和toFloor和当前的电梯系统图计算其最短路径,并将其最短路径的对应节点返回,若其最短路径的节点数等于2说明不用换乘;若大于2,说明乘客有必要换乘,根据其路径的节点对其fromFloor和toFloor进行对应修改,让其每次执行一段路径,这样是一定能匹配到一个适合的电梯的。这样就实现了换乘操作。

  • 在换乘电梯的时候,可能会由于维修电梯导致乘客的最短路径发生变化。这个只需要在每次维修电梯时更新相关乘客的所有最短路径,并重新进行分配即可。

  • 如何实现同一楼层中服务中和只接人的电梯个数限制。有了课程组实验课上提醒的Semaphore信号量类,就显得易如反掌,我建立了SephamoreTable类,里面有onService和justIn两个信号量表,分别表示每个楼层对应的服务中和只进人信号量。分别初始化每个楼层为4和2后,需要在每次电梯开门时对onService对应楼层的信号量进行acquire,并判断是否为只进人,如果是就对justIn对应楼层的信号量进行acquire;关门同理,对onService对应楼层的信号量进行release,并判断是否为只进人,如果是就对justIn对应楼层的信号量进行release。由于信号量是线程安全的,因此不需要添加额外的锁。

UML类图

在第二单元的三次作业中,我没有经历重构,因此在两次迭代后的最终代码UML类图如下。

elevator

UML协作图

UML协作图如下。
elevator

bug分析

这一单元我的三次作业强测成绩都在(85, 90)的区间内,遇到了很多bug。

第五次作业

强测与互测没有发现bug。但是由于我的疏忽,开门条件设置错误,导致我在如果电梯内有乘客且当前楼层没有乘客出去的时候,就不会开门接候程表里的人,这就导致无法捎带;然而中测数据并没有测出这个问题,我也有些大意,没有再去构造数据,计算运行时间。因此强测的性能分基本为0。

第六次作业

强测错了一个点,原因是新加的电梯没有考虑方向问题,导致其抵达了第0层。这也是因为我在第五次作业的电梯中改变方向的方法写的不仔细,没有考虑迭代问题,在迭代后忘记修改导致。

在提交时自己de出了一个比较重要的bug。如果维修电梯为最后一条请求,维修完后将该电梯的所有请求和乘客重新分配给其他电梯,但是这个时候已经结束了输入,在我的第五次作业的判断中,结束输入会导致所有电梯线程关闭,这就会使得这些重新分配的请求和乘客进入了已关闭的电梯线程中,无法抵达其目的楼层。我采用的方法是给电梯增添一个wake的布尔值,如果电梯已经关闭并且有请求加入候程表,就将电梯唤醒。(事后与其他同学交流,发现很多同学都没有考虑这个问题,导致强测出了bug)

互测没有测出bug。

第七次作业

中测数据过于简单,在提交的时候通过讨论区的评测机de出了两个bug。

  • 在判断是否为只接人电梯时,我一开始写出的形式为在acquire和release中都判断乘客是否有需要出去的。但是由于一些奇怪的原因,导致前后的判断条件可能不一致,这就导致一个电梯在开门前没有被判断成只接人电梯,但是关门后被判断为只接人电梯,这就导致没有acquire但是release了。发现了这个bug后就很好改,只需要设置一个flag值判断是否acquire,如果acquire过就必定release掉,反之就一定不release。

  • 在电梯的乘客交换中,忘记对候程表进行上锁,导致出现线程不安全问题。

遗憾的是,我在修第二个bug时明明注意到了两个地方均需要上锁,但在修改时大意,只修改了一个地方,强测刚好出现了另一个地方的两个问题,错了两个点:-<。

debug方法

多线程问题由于不可控原因,常常无法对bug进行复现,也不能通过IDE自带的debug功能进行debug。因此我主要选择的debug方法是静态调试法,在每个怀疑的地方使用System.out.println,从而找到问题所在,虽然效率低,但是比较好用

此外,讨论区中各位大神的评测机也帮助我测出了很多bug,在此狠狠地致谢。

心得体会

第二单元的多线程作业结束了,我从完全没接触过多线程编程的知识,到迭代设计了一个能正常运行的多线程电梯系统,基本掌握了多线程程序的设计方法,并且能够解决线程安全问题,对于出现共享对象被同时访问读写的情况,一定要进行上锁,不然会出现线程冲突问题,我在这里也栽了跟头。

在层次化设计中,要在开始写代码前进行代码框架的设计,考虑好未来的迭代方向和可能性,避免重构。一次又一次的迭代要求也提醒我始终在设计的时候遵循SOLID原则,始终提醒自己“高内聚低耦合”,以便拥有更好的迭代体验。

此外,多个小bug也提醒我在设计程序时要始终保持严谨,程序的测试不能因为中测通过而停止,而是多去考虑边界情况,自己构造针对性强、强度较大的数据,进行反复测试。

最后,感谢讨论区以及身边的同学在我遇到bug时提供的帮助。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值