BUAA-OO-UNIT 2 电梯调度

目录

前言

第五次作业

UML

类图

​编辑

时序图

线程设计

LOOK策略

同步块与锁

总结

第六次作业

UML

类图

时序图

调度方案

电梯重置

总结

第七次作业

UML

类图

时序图

向双轿电梯的转换

双轿电梯的运行(如何防止碰撞)

总分配策略的改变

总结

单元总结

同步块与锁

同步块

synchronized

ReentrantLock

ReadWriteLock

debug

防止轮询

性能分数/调度策略有感

心得体会

线程安全

层次化设计

线程设计

共享类设计

枚举类


前言

我们要模拟的电梯系统是一个类似北京航空航天大学新主楼的电梯系统,楼座内有多部电梯,电梯可以在楼座内1-11层之间运行。系统从标准输入中读入乘客请求信息(起点层,终点楼层),请求调度器会根据此时电梯运行情况(电梯所在楼层,运行方向等)将乘客请求合理分配给某部电梯,然后被分配请求的电梯会经过上下行,开关门,乘客进入/离开电梯等动作将乘客从起点层运送到终点层。

可以采用任何调度策略,即任意时刻,系统选择上下行动,是否在某层开关门,乘客分配给哪部电梯都可以自定义,只要保证在电梯系统运行时间不超过题目要求时间上限的前提下将所有的乘客送至目的地即可。

第五次作业

本次作业课程组没有将难度提高,甚至对于乘客进行了指定电梯这一操作,直接省去了调度方案这一步,只需要把电梯的运行和简单的投放做好即可。但是

万事开头难

UML

类图
时序图

使用生产者-消费者模式(借鉴于实验),策略使用LOOK算法。

线程设计

我是用的策略就是作业中推荐的最基础的实现方法即生产者-消费者模式,并没有引入超出课程组提示的思路。

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

调度器线程(Schedule)负责调度,把输入线程收到的请求不断的送到每个RequestQueue中。

对于每个电梯线程,我设置一个乘客请求表(RequestQueue),接受调度器线程给的信息,由电梯读取。

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

LOOK策略

参考学姐YannaのBlog博客,自我理解如下:

电梯开始的默认方向向上。

电梯的下一步依据策略不断进行判断。

开关门判断:

        如果发现电梯里有人可以出电梯(到达目的地),则开门让乘客出去;

        如果发现该楼层中有人想上电梯,并且目的地方向和电梯方向相同,则开门让这个乘客进入。

电梯是否继续运动判断:

        如果电梯里有人则继续保持运动状态即可。

如果请求队列不为空,且顺路方向以及顺路楼层有请求,则电梯继续沿着原来的方向运动。

如果请求队列不为空,且顺路方向以及顺路楼层都没有请求,则电梯掉头(掉头并不做别的任何事情)。

如果请求队列为空,且输入线程没有结束,则电梯停在该楼层等待请求输入(wait)。

如果请求队列为空,且输入线程已经结束,则电梯线程结束。

同步块与锁

由于本单元涉及的是多线程问题,会有线程安全相关的问题,要对于共享对象进行同步控制。

我的共享对象是Pool和RequestQueue(实际上可以不用分开但是我改变了存储方式所以写成了两个类),其中Pool会在输入和调度线程中被调用,而RequestQueue则是在电梯线程和调度线程中被调用。

本次作业中虽然了解到了有很多的锁,但是本次作业暂时使用的还是synchronized实现对同步块的控制。是在Pool和RequestQueue两个共享对象类中的所有读写方法都加了锁来保证线程安全。(写完就已经准备换读写锁了)

总结

本次作业属于从0到1的过程,所以还是有点痛苦的。

但是当对于锁和线程的理解到位之后,实际上完成本次作业是没有问题的。

对于策略问题,在指定电梯的情况下只有电梯运行策略这一个点值得考虑。但对于这一点,实际上任何策略本身都是有优缺点的,最终的表现状态如何还是要看数据点的特点。但是其中也有一些小范围的局部的优化值得去做,比如量子电梯等。

第六次作业

本次作业迭代部分可以总结为以下两点:

  • 增加了电梯重置,在重置后可以改变电梯的部分性能。
  • 取消了指派电梯的过程,相当于要建立一个调度方案。

UML

类图

时序图

调度方案

本次作业,我为了实现有关性能的提高(可能),进行了相关的重构,主要为以下内容。(以下只介绍与第五次不同之处)

我的第一次架构是大家公认的最健康的架构,即inputThread- Schedule- lift,其中InputThread负责将输入读入并传给Schedule,而Schedule负责调度,lift即电梯线程。本次作业要求实现的调度即可以在Schedule中实现,但是一个提前设定好的调度制度依我愚见都有明显的利弊,毕竟都是无法做到对于样例的预测,所以很难实现一个在接收到request的时候就能把它给一个最合适的lift。

所以,我想实现的是尽可能的实时一些,而不是一个写死的策略。

我了解到的相关的方式有局部贪心算法,简单来说就是模拟,在receive之前先算出电梯运行的时间,然后进行投放(当然计算是基于已经接收到的请求,所以最优的方式也是不考虑未来的)。这种方法的性能不错,但是不太好写。

我最后选择的是一种类似于自由竞争的方式。策略的基本逻辑是,每一个电梯首先都尝试从总队列中获取一个请求,并把这个当作总请求去实现。然后在路途中如果遇到可以携带的就直接从主请求队列中去携带。这样相当于每个电梯只有一个请求是分配的,别的都是实时获取的携带上的乘客。

所以就删除了Schedule这个线程,让lift去直接从总队列中获取,大概算是自由竞争的一种。

电梯重置

电梯重置的实现相对于调度方案来说稍微简单一些。

首先是要进行电梯性能的可变性设置,因为第一次作业中这些参数是不可改变的。

其次就是实现电梯重置前的过程。这里实际上也有策略可言,因为课程组并没有严格要求必须收到消息后立即开始reset,而是允许有两个arrive,但是限制了时间为5s(accept 到 end)。所以在时间限制内是可以做一些事情的,比如继续送乘客送两层等等。但是这样的选择并不意味着会快,有时候尽快开始reset性能会更好。

重置发生时需要注意的有:

- 将队列退回时要在Begin之后(感觉好多同学都在刚开始写的时候遇到这个问题,包括我)

- 在电梯里面的乘客要先out才能重新进入主队列,而且进入主队列时要对于request进行更新,即当前楼层变为出发楼层。(当然,到目的地的直接就不用加入主队列了)

- 如果实现了影子电梯,要记得在上面加个限制条件即没收到重置信号

- 还要注意线程安全,防止有同时输出的并且先后顺序弄反的地方

其他的都是线程之间的信号传递了,我的架构在这方面感觉没什么问题,把重置优先级设置成最高就好了

总结

本次作业的难度应该来说取决于你选择的调度策略和reset策略,优化的同时往往伴随着更多的秘密bug,并且优化也不一定能保证正向的,所以感觉还是选择一个自己想写的写下去就好。(无脑random分配也是一种可取的中上策略呢)

还有就是有关于线程安全的问题,这个在第一次作业中只要架构合适基本不会出现什么大问题,但是在第二次作业中就不一样了,共享对象被调用的地方更多,死锁和没有唤醒的情况更多了,所以在写的时候一定要清楚地考虑这几点:

  • 共享对象读写访问要安全
  • 线程什么时候等待,等待后谁来唤醒
  • 在不同线程不要以不同的顺序访问共享对象

当然值得注意的点有很多,还是要根据自己的架构进行代码的编写。

第七次作业

本次作业课程组似乎进行了创新,迭代内容是往届不曾有过的船新版本。

即增加了一种reset,可以将电梯在1.2秒内变身为双轿电梯,即可以理解为一个楼道中有两个电梯A和B同时运作。

当然要完成这次迭代,具体为以下几部分要完成:

  • 实现向双轿电梯的转化
  • 实现双轿电梯的运行
  • 实现调度策略的改变

UML

类图

时序图

向双轿电梯的转换

这里我采用的是一种较为直接的方式,即在reset后直接将Lift的原本线程结束,然后Lift就化身为两个子电梯的调度器(非线程),对于其他的电梯线程和InputThread来说,在reset前获取这个电梯的信息和reset后通过的接口是不变的。这样相当于就在Lift内部做工作就好,对于外部没有任何改变。

如果选择这样做需要注意的点:

  • Lift对外的接口对于第二类reset前后要进行分类,以保证其他线程能获取准确的信息。
  • Lift在reset最后切换状态的时候(我设定是isRepairing和isTwo),要留意和其他操作(如将缓冲队列中的请求放入A和B电梯还有清空缓冲队列等)之间的顺序。

双轿电梯的运行(如何防止碰撞)

因为是最后一次迭代了,所以为了图省事,就新建了两个类LiftA和LiftB,还有其对应的策略StrategyA和StrategyB,其实区别很小,就是A在下面,B在上面,所以实际上可以合并为一个类,然后在某些地方分类讨论即可。

运行策略我是主体上继承了Lift的策略,即LOOK。

双轿电梯需要实现的一个功能是交换,而且两个子电梯不能同时出现在同一层,所以在这两个子电梯中我新加了exchange的操作,exchange的操作中我直接将进入交换层、在交换层中放人和接人、出交换层变成一个连贯不可分割的操作,这样就不会有电梯停在其中的情景出现。

两个子电梯不同时出现在一层我是通过Lift作为调度工具进行调节,保证只有一个子电梯会出现在交换层。

当然这个地方也借鉴了讨论区的一个优化,即另一个电梯如果等待时间大于等于移动一次的时间可以在另一个子电梯出交换层的一瞬间直接进去。

总分配策略的改变

实际上在进行这次作业的迭代前,我首先狠狠重构了一番。

为上一次使用自由竞争忏悔三秒= =。自由竞争由于过于耗电,导致虽然速度都算上乘,但因为耗电量性能分低甚。故舍弃之,退回第五次作业的架构,即对请求进行分配而不是争抢。

具体如何分配,采用的是评分。具体考虑的点有:超过personLimit的人数和请求的时间代价还有是否正在重置。

因为这次作业由新增了双轿电梯,故油增加了一些关于本次作业的设定,即如果是双轿电梯,则在计算方式上进行更新,使分配更倾向于双轿电梯。(因为在性能分中的三条,双轿电梯只有稍微降低一个乘客的等待时间的可能性,对于另外两个只会提高而且幅度明显)

总结

双轿电梯本身不算困难,在熟悉了多线程之后完成迭代功能还是比较简单的,在某些关键代码区即临界区前后对于相关代码顺序的排布稍微细心一些即可。

还有就是架构问题,下次绝对不对课程组强烈推荐的架构进行自以为是的改动。那么就再次为第六次作业改成自由竞争忏悔两秒。

单元总结

多线程的编写可真是费脑筋啊 =w=

同步块与锁

同步块

同步块是通过锁定一个指定的对象,来保证同步块中的代码是同步的。

有关同步块的划分,我是秉持了两个原则。

首先是共享对象的读和写操作,有影响正确性的潜在能力的都划分到同步块中。因为这些部分,会因为执行顺序的不同而影响正确性,所以必须要划分到同步块内来保证正确。

其次就是保证第一个原则的情况下,尽可能的减少同步块中的内容。如基本类型赋值和引用类型赋值等原子操作,还有不可变对象不用同步。

synchronized

这个是最基本的一种加锁方式,好处就是使用方便,直接加上这个关键字就可以使用并且会自动释放锁,缺点就是如果要使用就需要把对象传来传去,缺少灵活性,并且获取释放锁比较死板,容易发生死锁。我是在第五次作业中采取的这种加锁方式。

ReentrantLock

ReentrantLock可以替代synchronized进行同步,并且ReentrantLock获取锁更加安全。

在获取到锁后,会进入try{...}代码块,最后在finally里面释放锁。

并且可以使用tryLock()尝试获取锁。

if (lock.tryLock(1, TimeUnit.SECONDS)) {
    try {
        ...
    } finally {
        lock.unlock();
    }
}
ReadWriteLock

使用ReadWriteLock可以提高读取效率。ReadWriteLock只允许一个线程进行写操作,但是允许在没有写操作的时候有多个读操作。

这种方式兼具了前面的优势并且更为灵活快速,所以我选取这种方式,唯一可能的缺点就是稍显复杂。

debug

多线程中的bug可谓是波诡云谲,变化莫测,让人如雾里看花。

我遇到的bug主要分为几个方面。

  • 对于线程结束的判断失误导致有的线程不能结束或者过早结束
  • 相关输出顺序不符合课程组要求
  • 线程共享对象的状态变化语句的顺序不当产生了小概率复现bug

多线程的编程中,断点式的debug方式已经不再适用了,可以试试

  • println大法,即在你想知道信息的地点加上输出进行观察
  • 通过stderr抛出异常,这样不会影响评测结果的判定

防止轮询

轮询是一种非常占用CPU的行为,具体体现在评测中就是你的CPU时间会超时。

解决轮询就会让你的编程增加一点难度,我是用的是notify-wait的方式,这种方式在编写中要注意的是

  • wait之后一定有notify进行唤醒
  • notify只在有必要的地方出现(我的编程中就只有三处使用)
  • 一定要用notifyAll而不是notify

性能分数/调度策略有感

经过血和泪的教训,深刻领悟一个道理——大智若愚 = =

性能分数主要是系统等待时间,等待时间和期望时间差值的最大值和耗电量。想要拿高分就要尽量实现总运行时间和每个人等待时间都要尽可能的短,并且减少电梯的没有必要的操作。

复杂度很高的影子电梯和自由竞争等策略,会耗费大量编程时间,然后带来的性能加分不多甚至可能是负加分。方便又不错的策略有random分,调参评分,模六等极简操作。

我是在第六次作业中自作聪明写的自由竞争,然后发现成为小丑后又毅然决然地反向重构加入了调参评分,并在最后一次作业中得到了不错的性能分。

所以还是荣老师的那句话:“不要因为性能而牺牲其他,除非性能有明确要求。”

心得体会

线程安全

在编写多线程程序的时候,用老师的话说,思维就应该像是几条并行的线,去实现

  • 共享对象只允许同时读取,而不允许写读或写写
  • 不同线程不能以不同的顺序去获得不同的锁,否则会有死锁产生
  • 不同线程之间不会发生不允许的影响结果正确性的顺序,即保证无论运行多少次,不会影响结果的正确
层次化设计
线程设计
  • 输入及调度线程
  • 电梯线程

当然按照课程组推荐的三线程(即输入和调度分为两个线程)也是可以的,相关功能会区别的更开,但是可能会产生的问题要注意的地方可能会更多一点。

共享类设计
  • 请求队列

我就只有这一类共享类,当然充当的角色不止一个,如总队列,每个电梯的等待队列等。

枚举类
  • 电梯策略

Instruction中我是设计了六种策略的枚举,方便strategy使用。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值