301
社区成员
发帖
与我相关
我的任务
分享前言
第二单元总体思维难度低于第一单元,但在debug时的不方便与多线程带来的不确定性,给找出bug增加了很大的难度,在解决一个问题的同时可能再运行一次发现了另一个问题,但是此时笨人没有意识到这是另一个问题,直接就蒙了。
三次作业框架
第一次作业:
本次作业主要参考了实验代码,共8个线程,分别为6个电梯线程、1个输入线程(InputThread)以及1个调度线程(Schedule),总体思路是输入线程负责读入请求,与调度线程共享总等待队列waitQueue。在检测到输入结束后,将waitQueue的isEnd设置为true,代表不再有新的请求加入。
调度线程则负责按这些请求所指定的电梯投入到电梯队列里,若检测到waitQueue为空并且输入已结束,则通过break跳出while循环来关闭调度线程。每个电梯线程属性包含在等待请求队列(processingQueue)以及正在处理的(即在电梯内)请求队列(inElevator)。
第一次由于不需要考虑电梯分配问题,所以调度策略较简单。本人采用的是ALS策略,考虑同向捎带。在第一次作业中,有影子电梯的做法,可以有一定程度优化,但是所有的优化本质上都只能做到局部最最优,如果刻意捏数据可以做到让任何分配策略达到最优。比如如果慢些关门,可能有乘客恰好在慢的那部分时间内到达,同时满足捎带条件而进入电梯,反而节约了时间。
第一次作业互测就有人提出用在50秒时大量数据涌入,此时很多同学的都会将请求全部分配给第一号电梯,导致最终超时问题。
总体代码结构如下

第二次作业:
本次作业不再指定电梯,并新加入Reset指令,即电梯重置,期间不能有乘客在内,并需要1.2秒,重置后电梯运行速度、最大容量都将改变,楼层则与重置开始时一样。需要注意的是当接受到重置请求后,需要尽快停下重置,即至多输出一次ARRIVE,并且从接受到重置完成总时间不能超过5秒。
本次作业需要将乘客分一定优先级,能同方向捎带的自然优先级最高,其余采用偷懒的平均分配策略。这里由于电量也是性能分的一部分,因此有一种方式是优先向一号电梯塞人,然后设定一个人数上限,因为怕被卡数据超时嘛。这样的好处就是多个人一起送,节约了很多电能,实测效果异常好。
本次作业没有什么大改动,主要是调度策略的设计,但出现线程死锁或轮询的概率大大提高,例如若在删除已处理请求顺序错误,则会导致调度线程提前关闭,出现请求回到总等待队列但不再进行分配的情况。
在优化过程中,有同学提出了可以深克隆电梯,预先演算一遍取最优。我个人认为这样的优化比较复杂,也容易走极端,优化不一定要极致才是最好,适当就行,当然我懒是主要因素。
在debug中,很多同学也遇到了需要高强度重复上千次才能复现几次的线程安全问题,这也是在课上提到的读写冲突,解决方式是在判断条件后,拿锁,然后再判断一次条件,才能保证此时的条件必定不再被修改。在研讨课上,同组同学还提到自己浅克隆导致在删除请求时漏删了的问题。
总体代码结构图如下

第三次作业
本次作业是三次作业中最困难的一次,加入了双轿厢电梯,即一个电梯分裂成两个在同一楼道运行的电梯,设置一个交换楼层,在下面的电梯的最高楼层即交换楼层,在上面的电梯的最低楼层也是交换楼层。
本次作业在调度策略上有一些改动,若有电梯可以不通过交换层直接将人送到,优先分配给此电梯,其余的策略仍大致与第二次作业相同。当然也有同学采用调参的方式,即对电量的代价与运行时间的代价分别设置参数,再通过实际情况的模拟,取性能较优的取值。
在解决双轿厢不碰撞问题时,对于一般情况,这里简单地采用如果某A/B电梯内无人且未收到请求,则判断是否处于交换层,是则移开。对于某时刻电梯AB都有人且都将到达交换层,这里设置一个新的类ConFloor,记录两电梯的楼层以及交换层,在每次电梯希望改变楼层时,一定需要先拿ConFloor的锁再移动,即A若先拿到锁,B将卡在用到ConFloor那一步,而当A到达交换层后,B将判断出交换层已经有A而不会再运行到交换层,以此避免双轿厢的碰撞。
本次作业遇到的bug非常难测出,就算已知自己的bug是调度线程提前关闭,导致后面A/B电梯在交换层将人放下后,不再分配给其他电梯,使请求请求人未被送到。这里正常思路是将线程的关闭条件改为若此时不再有人,即全部送到后再关闭,但由于在实现的时候遇到了电梯内请求序列类被电梯进程拿锁,总等待队列被调度线程拿锁的死锁情况发生,这一方案似乎需要该很多内容。因此,这里先固定将调度线程在输入线程结束后的30(或50)秒后再关闭,简单粗暴但是意外地可行。
总体代码结构如下

在debug时较好的方式是采用日志,但这里是采用了print的方式一行一行去看运行了哪里没运行哪里,对于可能涉及到锁的对象需要在其前后都设置如输出一个标志,以此判断是否卡在了锁上。另外,由于各线程一直处于一个while循环内,如果不加条件地输出一个内容将会满屏全是这个数,但很多时候是希望隔几秒输出一次,这时候条件设置就极为不方便,加上多线程本身的难以复现性,使这时的工作量极大,远超单纯完成中测的工作量。
本人的电梯线程是一个重类,在未创建方法类前曾达到过700多行,下面是我的UML生成的结构图。

心得体会
本单元是第一次接触多线程问题,电梯调度问题也是多线程问题的一个特别经典的案例。值得一说的首先是线程安全问题的体会。无论课上或是课下的debug,线程安全是一直需要被关注的问题,如读写冲突,懒汉与饿汉的问题,都需要相应的方式去解决。这里比较遗憾的是在第二次上机时提供了读写锁的方法,但是在作业中却未找到很好的思路或说是方法去运用,仍采用的是synchronized方法,虽说读写锁本质上也是通过synchronized实现的,但是这样一来代码就没那么优雅也少了一次锻炼的机会。
其次是在调度设计方面的体会,这里大家的设计主要有两种,有调度线程与无调度线程,一般来说,不设置调度线程的代码会简单一些,但是效果是一样的。对于电梯的具体运行实现普遍采用生产者-消费者模型架构,并不需要做过多修改。