任务要求:通过多线程的互斥和同步控制,实现电梯功能模拟,并进行扩展和迭代。
第一次作业:单电梯,由于请求完成时间有限制,所以需要进行捎带或者其他优化策略。
第二次作业:多部电梯,增加每部电梯人数限制,到达楼层可扩展到负数层。
第三次作业:多部电梯,每部电梯可到达楼层、速度、人数限制不同。需要支持乘客换乘。可以在运行过程中增加电梯。
一、多线程电梯程序设计策略
第一次作业
大致思路为一个最最基本的生产者消费者模型。一共两个线程,其中请求输入类作为生产者线程,电梯类作为消费者线程,调度器作为共享变量管理请求队列。作为生产者,请求输入类会将请求加入到调度器的请求队列中,电梯类则作为消费者根据请求队列中的请求进行运行,并在相应层将人加入到电梯当中,成为电梯队列的一元。注意,这里我并没有将电梯中人组成的队列当成请求队列,因为我并不关心队列顺序,这和我的调度算法有关。所以说我一共只有一个请求队列,维护起来十分方便。
关于调度算法,我只根据请求队列里的请求以及电梯内的人的请求来对电梯运行的方向进行转换。当电梯和请求队列中都没有人时,电梯类的 direction 变量置为0,并 wait() 。当电梯暂停,即为 direction 变量为0时,根据当前请求列表的第一个请求的所在楼层,改变电梯的方向,即 direction 的值,以使其向请求所在楼层运行。在电梯运行过程中,只有当前运行方向上仍有请求队列中请求的所在层,或者有当前电梯中的人的目标层,那么便不改变运行方向,否则使电梯反向运行或停止。每当电梯在运行时经过一层,都会扫描电梯内部有无要出去的人,或者当前层需要进电梯的人。只要二者满足其一便开门,出人进人,关门。这个调度算法实现简单,且性能不错,后来我才知道原来这就是look算法。
第二次作业
第二次作业的迭代需要新加的代码非常少。首先,为满足可以到达负数层的,在电梯类中加入一个楼层与连续整数的映射关系。其次,为实现多部电梯,只需开启多个线程即可。最麻烦的一步是设置电梯限制(其实也不麻烦)。在扫描请求队列中是否有请求在电梯的运行方向时,需要提前判断电梯是否已满,如果电梯已满,便忽略请求队列中的请求。而在每一层进人时,如果电梯已经满了,就不再进人了。
这样下来,仍然是一个简单的生产者消费者模型,调度器甚至还是只用一个(我三次作业调度器都是单例模式)。由于多个电梯线程自己跑自己的,对于请求的竞争相当公平,相当于请求会平均分配给各个电梯,可以充分利用所有电梯。最后数据的性能分没有低于99的,还有几个100,性能出乎意料的好。
第三次作业
第三次作业的迭代需要新加的代码依然非常少,这一单元的可扩展性做的真的不错,不像第一单元每次都是整个重写。我仍然是只有一个调度器,调度器不会分配请求给电梯,每个电梯还是自己跑自己的。也因为这样,加入新电梯的任务十分简单,只需要直接新开启一个线程。在电梯对象的构造方法中,根据电梯类型对数据(包括可到达楼层,速度,人数限制)进行初始化。在开门前加入判断,当前楼层是否为该电梯可到达楼层,是的话才开门。另几处需要更改的地方:扫描请求队列中在电梯运行方向上的请求时,忽略不能到达的楼层,同理在判断是否要停下时,也要忽略不能到达楼层的请求。除此之外,电梯仍然还在第一次作业中一模一样的调度算法在可到达的楼层直接来回扫描即可。
对于实现换成,我在 Person 类中加入了 destination 变量,初始为0。在请求加入队列时便判断是否必须换乘,若必须换乘,则将 destination 赋值为 tofloor (目标层),将 tofloor 赋值为要换乘的层。在人出电梯时,判断 destination是否为0,若不为0,则将 tofloor 改为 destination , destination 置0,重新加入到请求队列中。
该方法不仅实现简便(调度器不用分级,直接让电梯自己跑,不用处理请求分配),而且强测中性能表现十分优异,性能分基本没有低于99.8的(除了两个弱智bug,某种电梯写错楼层了...),非常出人意料。
二、度量分析
这里只分析第三次作业,三次作业的相似性非常强,从第一次到第三次都没有新增类,只是在各个类中新增了方法和数据。
UML类图
可以看出逻辑结构十分清晰,调度器作为共享变量来对线程进行互斥和同步,因此方法众多,稍显冗余,且有些方法重复性高,其实可以通过重载overload进行合并。不同类直接分工较为明确,可扩展性强,感觉整体架构还是不错的
度量分析
Elevator.setDirection() 方法的控制语句最多,设计复杂度iv(G)和圈复杂度v(G)最高,用于根据请求队列和电梯内队列判断是否改变方向,是调度算法中最最核心的部分,复杂不可避免。方法的非结构化程度基本都不高,圈复杂度v(G)和模块设计复杂度iv(G)除 Elevator.setDirection()以外基本表现良好。
可以看出每个类内聚性都不错,调度器类稍差,但由于其中有很多控制线程安全的方法,也是可以接受的。可以看出调度器类和电梯类完成了大部分工作,说明该两类的方法应该做进一步拆分。
SOLID原则分析
三、Bugs & Tests
我承认这单元偷懒了,没有进行必要的测试,导致一些非常弱的bug产生,让我十分后悔。因此我也没有hack别人,整个就摸了。所以这里就来说说我的反思。
第一次作业被hack出一个CTLE的bug,原因是因为使用了暴力轮询来使电梯获取调度器请求队列内的请求,不停 while (true) 消耗太多cpu资源。后来在请求队列没有请求的时候 wait() 一下就好了。反思:多看讨论区的讨论,在de这个bug之前我都不知道暴力轮询是啥。
第二次作业是一个特例的bug,即只要最开始在第一层开门进人,那么所有没有竞争到请求的电梯都会在原地(1层)ARRIVE 一下,which是不应该的。其实这就是一个分支控制的小问题,而且1层开门这种数据太弱,实在不应该没有测试到,就算自己搞自动化测评,遵循覆盖原则也应该手动构造出这类数据,实在是不应该,白得优异的性能,真的是教训。
第三次作业的bug更弱,原因是C类电梯的换乘出了小错误,且仅在从3层上电梯,去往4层时会出现这个bug。无奈这一小种情况仍被强测两个数据逮住了。我想这种数据想要自己测出来唯有测评机大量跑了,这么看来自动测评还是很有必要的。
四、心得体会
这单元主要是多线程安全的训练,但我在写程序的过程中并没有太多遇到线程安全的问题,甚至写代码的有效性要比上一单元高。我的理解是一定要想清楚你线程运行的时序,什么时候停,什么时候唤醒,由谁来唤醒,这些问题想好之后,其他的事情迎刃而解。此外在写下每一个共享对象时都在脑子里过一遍这个对象在其他线程中可能会进行哪些操作,将其依照时序排列组合一番,确定自己正在写下的操作在任何一种情况下都不会出问题才能放心写下,这正是我在进行迭代修改的过程中所做的,根据所要新加的功能整体过一遍代码,过完也就改完了。