301
社区成员
发帖
与我相关
我的任务
分享在本单元中,由于多个线程可能共享同一个对象,都会对它进行添加或删除元素的操作,所以线程安全问题就显得十分重要。首先对于请求队列类,其中的一些方法必须上锁,比如addRequest、removeRequest、getOneAndRemove、setEnd、isEnd等,此时上锁的对象应该是它本身,而我的设计还有ResetQueue类,与RequestQueue类相同,也应对相应方法上锁.应注意,不是所有的上锁方法都需要notifyAll,例如isEnd方法需要上锁保证是否结束状态不被改变,但并不需要在判断后进行notifyAll,这是因为我们的设计在加入元素、删除元素和setEnd方法中正确的加入了notifyAll,使得程序正常的运行可以唤醒等待中的线程,而isEnd方法在每次循环中都会多次使用判断,会增加很多次不合理的唤醒,可能会导致运行无法结束等问题。
而对于“唤醒”,还应该有如下考虑,比如我的程序,电梯以自身的分发队列(从总的waitQueue传入各个电梯)作为自身wait时的锁,所以在添加元素时,如果是来了Request就可以正常通过addRequest唤醒,但如果来了ResetRequest就应单独写方法对其进行唤醒。还有在每个电梯进入自己的wait之前应该对调度器进行唤醒,而调度器以总的waitQueue为锁,所以也应该单独写方法对其唤醒。
既然谈到了同步块的设置与锁,那我们就进一步探讨在第三次作业中,双轿厢电梯对于换乘层的处理。我的处理是对于下层电梯,当它到达换成层下方且处于关门状态时,判断其是否需要进入换乘层(方向向上,且电梯中有人或换乘层有向下请求),如果有,将其进入换乘层上下客并下到换乘层下一层的操作变成同步块,合并上锁;对于上层电梯同理。但此时会出现了一个问题,就是这些操作进行的时间比较长,占用锁的时间也比较久,我一开始采取了换乘层的等待队列作为锁(比较符合逻辑),后来发现这样导致想往换乘层等待队列加请求时被卡住,导致程序整体变慢甚至reset时间过长。而后我就单独创建了一个锁,上下双轿厢电梯共用,避免影响程序其他部分。
调度是本单元作业很重要的一大部分,直接决定了能否通过评测和性能,调度设计不好,可能只是一小点没有考虑到就可能让程序超时。首先在第一次作业中不需要考虑调度,甚至不需要写调度类,因为请求已经按照电梯Id分配好了。第二次作业开始就要考虑调度了,我采取的思路主要是评价函数,通过电梯当前的各个参数,结合请求进行分配。
需要注意的是一开始我并不会把请求分给正在reset中的电梯,但我没有考虑到如果此时只有一部电梯不在reset那所有请求都会分配给它,导致超时。所以分配时也应考虑到正在reset中的电梯,但它们并不能立即receive请求,因为正在reset,于是就要新建一个buffer队列,先加入其buffer队列,在reset结束后再加入正常的分发队列。
下面展开说我的评价策略,总思路是选取分数最少的电梯,对其指标的评价每有一项不合格就进行一定程度的加分。首先要考虑的就是人数问题如果超载加10000分,且每超载一人加10000分(此处的超载不是指电梯里人数超载,而是电梯里人数加等待队列里人数);其次应该考虑电梯能否捎带(第二次作业),不能捎带的电梯加2000分;然后分方向和电梯当前楼层与目标from楼层之间的距离(第二次作业)进行得分计算;还应考虑电梯运行速度(+200*speed)和电梯是否在reset(+40)等。对于第三次作业,我将变为双电梯的电梯的楼层设为1,方向设为向下,这样就不能对任何请求进行捎带,而由于双轿厢电梯有一定的性能优势(耗电少),所以对于非双轿厢电梯还会有一定的加分(+2000)。
在这样的设计下,电梯保持一个合理的分配调度,在第六次作业强测中获得99.4分,第七次中获得97.9分。

第一次作业架构:
第一次作业架构比较简单,由输入类对waitQueue进行添加,schedule类根据请求指定的电梯Id进行分发,对于电梯的设计,我将每个电梯作为线程,而每个电梯有自己的策略类,策略类负责控制电梯的行为,电梯作为载体进行运行,策略类不单开线程,电梯类只需要得到自己的分发队列即可,不需要获得总的等待队列。

第二次作业架构从类上并没有改变多少,只加了resetQueue,模仿请求队列。但实现上有较大差别。输入类负责解析输入,判断是Request还是resetRequest,并加入相应队列。对于schedule类改动较大,负责将总请求队列按得分发给各个电梯,还需要对resetRequest进行分发。而电梯的策略只需要加入reset方法,在接受到reset请求后进行开门放人等操作,电梯在reset之后,自身乘客及自身等待队列里的乘客都会进入总等待队列,同时不要忘记将buffer队列中的请求加入自身等待队列。

对于第三次作业,架构从类上也并没有改变太多,最大的改变是加入双轿厢电梯类,继承电梯类,双轿厢电梯的上部分和下部分有单独的运行策略,且要避免两电梯在换成楼层相撞。注意,电梯到达换成楼层后,乘客最好重新回到总请求队列,而不是直接给双轿厢电梯的另一部,这样性能会得到进一步优化。还有其他变化比如电梯中加入新的reset方法等。还在评价电梯算分时进行了部分改动,保证之前的架构尽量不变。
再迭代中我尽量保持先前的架构不变,在此基础上增添内容以保证架构的可扩展性,由于职责分配明确,输入类负责输入,调度类负责分配请求,电梯作为线程,各自拥有策略,若要求改变可以通过更换电梯策略来实现。而对于双轿厢电梯,由于我在其类中单独实现运行方法,所以更改起来比较麻烦,此部分可扩展性较差。对于本单元作业,扩展时线程安全的保证也相当重要,对共享对象进行的操作都需要加锁,因此对于锁的选择以及加锁的同步块也需要慎重考虑,尤其是牵扯到wait的部分,保证wait后一定会被唤醒。

类协作图如上,体现基本架构
多线程的bug主要出现在线程安全方面,因此和下面的关于线程安全的心得体会一并说。在第一次作业中一般不太会出现线程安全的问题,这是因为没有reset,电梯里不需要获取总请求队列,更不需要以它当锁。但从第二次作业开始就会产生一系列的线程安全问题,首先是CTLE,这是最好定位的bug,即程序中未在需要wait时进行wait导致轮询,分别排查schedule和电梯的wait即可。然后是由非线程安全问题导致的wrong answer,一般根据错误输出进行调整。最困难的是由线程安全问题导致的bug,极有可能产生RTLE,这类bug我的解决方案是先在本地进行复现,确认无法停止后,一般是某个线程正在wait而其他线程无法唤醒它,此时就先对调度器进行排查,用输出大法看最后是卡在哪个地方,同样用输出大法看各个电梯卡在哪个阶段,从而对症下药。
比如在电梯wait前会先唤醒调度器,而此时若调度器已经不会再分配请求给电梯,调度器理应结束,通过setEnd唤醒wait电梯,然后电梯运行完后结束,但若此时调度器先setEnd电梯随后才wait,就导致无人唤醒它,程序无法结束,因此应对电梯wait过程加一个锁,在锁里先判断是否已经被setEnd,若已经被setEnd则无需wait。
对最后一次作业bug可能更加难找,因为双轿厢电梯还会使用新锁,正如上文提到的我双轿厢电梯上下共用共享楼层的等待队列作为锁,在其他进程想访问时发现锁被占用就要被迫等待,造成大量时间浪费。我最终通过输出法逐步排查定位到了卡住的地方,通过更换锁解决了这个问题。
最后再分享一点心得体会,第二单元的作业难点在于线程安全总是感觉难以得到保障,但在经历过不断改bug之后,不仅对线程安全的理解更加深入了,还丰富了自己修改bug的能力。这一单元经历了被刀,体会到改bug的不易,不过苦尽甘来,总算熬过了这一单元,也算收获满满。