301
社区成员
发帖
与我相关
我的任务
分享第二单元中,我们实现了多线程交互为基础的电梯系统,主要着重的为Java多线程的使用方法、多线程特性以及线程安全的编程方法。作业中通过线程间的交互完成不同电梯线程的配合运作,高效地完成任务。个人感觉,这一单元的任务从某些方面来说比第一单元更加简单(Eg:逻辑上的dbug),但是,多线程的不确定性也带来了更多的问题,成为了一些新的麻烦,也需要全新的解决方法。
虽然这单元三次作业的情况仍不甚理想,但是相比于第一单元情况,也算是有所进步了,至少能够蹒跚地完成三次作业了,后面继续学习继续进步吧。
以下为这单元三次作业的总结与分析,具体包括每次作业的实现思路、实现的过程、出现的问题与解决方法。
第一次作业更加注重于电梯自身的设计,对于schedule类的设计较少,因为已经为你分配好了应该去接送每个人的电梯。于是这次作业的重点便放在了电梯自身自治能力的设计,比如当你把一堆能捎带和不能捎带的人塞给电梯的时候,电梯怎么能够在这堆人中筛选出他能够带上的人,进而正常运行。
该次作业的UML类图如下:
UML协作图如下:
构造思路:
这次作业初次构造的时候与最后的成品差距较大,一开始思考的时候其实准备复用官方包里面提供的Request类,形成RequestQueue的类以后,可以作为生产者-消费者模型中作为中间的“托盘”使用。比如在inputThread与schdule类中间可以简单使用,schedule类与电梯类中间也可以进行简单使用。
但是实际构造中,为了电梯控制的直观与高效,没有复用这个Request类,因为每次从中提取请求,需要遍历所有成员元素,有点低效与不直观。我采用了一个HashMap对每层楼的情况进行存储,可以较为方便地观察电梯目前所在楼层请求情况。但是自然这样也有一定缺点,比如失去了我们在直接使用ArrayList进行类似压栈弹栈操作时候能够保持的时间先后顺序,保证对请求的“先来先服务”。
本单元作业采用的均是生产者-消费者模型,生产者即为输入线程Schedule,消费者是六个Elevator线程,或者为InputThread与Schedule之间的交互,其实主要共享出现在Schedule与Elevator中间的高频交互。
因此,只需要对两组中间的共享内容进行互斥即可,需要互斥的场合为出现读写冲突与读读冲突的情况。直接对这种情况的变量与方法加上synchronized即可。这种为全互斥锁,读读也会进行互斥,虽然效率按理来说低一些,但是也足够了。
本次作业中基本没有涉及到外部的请求分配问题,因为已经指定了每个人用哪个电梯去接他,更多的策略来自于电梯内部的调度,主要关于“以什么顺序去哪里接哪个人”的问题。外部schedule类只是负责识别请求里面的要求电梯,当个发牌员就好了,不需要有很多自己的“思想”。
这个迭代策略也确实能够保证电梯自身的“健壮性”,比如后期调度器的问题把人分配“错了”电梯,这个“错误的”电梯主要指的可能一个对这个请求不是那么高效的电梯,或者是他现在不能捎带上的请求。对于一个健壮的电梯,他完全可以把当前的处理完了以后回头来继续处理这个请求,也不失为一种策略。
本次作业的bug情况还是挺严重的,主要来源于严重的逻辑bug,线程同步这块倒是因为保守的上锁策略所以没有出现过多的bug。
具体bug产生在比如电梯没有能够正常停止在存在的楼层内,跑到了1层以下以及11层以上的问题,通过解决这些问题也增加了电梯自己内部调度策略本身的健壮性。
本次作业主要取消了人员输入时候指定电梯的过程,因此需要具有一个外部的分配策略了。其次是出现了reset指令,指令中会更改电梯的满载人数,运动速度的参数,电梯需要在接到reset指令后以最快速度停住,开门,放人,关门,执行属性的改变。因此,第一次设计的ElevatorSetting对象起到了重要的作用,这个对象保存了电梯的基本参数,能够提供足够的与外部交互的条件,而不需要调用每个电梯内部方法进行参数的更改以及指令的下达了。但是在最初的提交中,ElevatorSetting这个对象与电梯的待处理人员对象是相互独立的,这会带来一定的问题,具体会在后续的bug分析中进行说明,以下分析阶段均使用最后结果的代码进行相应分析。
本次作业的UML类图如下:
UML协作图如下:
设计思路:
其实注册电梯,不一定就是注册一个电梯类,也可以很简单地注册一个电梯的信息表,或者称为“回执”,Schedule类只需要去对这个回执进行管理即可。因为后续在调度策略中,一定程度上使用了就近分配的原则,但是每个电梯的速度不同,当前楼层不同,满载人数不同,这些都需要加以考虑,所以距离变成了“时间距离”。即楼层差值与运行速度的乘积,所有这些如果每个都是一个对电梯的对象里面的get方法,其实有点不太美观,而且可扩展性也没那么强,万一后面还要求改其他参数呢?所以就设计了这个ElevatorSetting类,可以方便电梯读取自己的运行参数,也可以方便外部对电梯进行评估。
其次是reset的计数器,schedule需要知道自己分发出去了多少个reset请求,有多少个已经完成了,多少个还没完成,还没完成没清空之前肯定是不允许休息的,不然就乱套了,因此InputThread与Schledule之间共享的Request类就遭殃了,现在它变成了InputThread与Schledule与Elevator共享的对象,Elevator需要在reset过程中往其内部回填未完成的指令,需要在里面加加减减计数器。计数器处理倒方便,Schedule拿出来一个reset加一个,电梯完成一个call一下减一个。
至于reset的操作,就是一整套流程了,到了一层,要reset,看要不要放人,要,开门,放人,把请求塞回去,关门,开始reset。不用放人,则直接回填请求,开始reset即可。使用状态机会很方便地能转移至该状态,并能正常出来。
本次作业其实真正共享的,就只有两个对象,一个是Request类,被三种线程轮流访问,但是好在频率并不高,直接全互斥就好了,也是保证安全的最佳手段。
其次是ElevatorSetting这个对象,因为电梯的待处理人员,其实也集成到了这个对象中,Schedule会根据这个对象的设定参数评估电梯状态,是否能够分配,电梯内部会在每次执行的时候根据这个对象里面的参数进行运行,电梯的可扩展性好了不少,至于电梯内部人员的ElevatorRequest类,则仍为一个电梯的自有类,其实不需要互斥了,因为在ElevatorSeeting中,对外部成员直接使用了HashMap管理,没有再套一层类了,也避免不直观。其实ElevatorRequest类中,需要方法在作业一的设计中,也主要是为了作为未处理的Person类存在的,也算是为它减负了。
本次作业不再指定电梯,因此需要自己写调度。
我采用的调度策略挺简单,简单来说分为三层:一层为遍历一遍所有电梯,寻找当前在运行的,任务不重的(待处理+电梯中的人数不超过电梯满载量的两倍),能捎带的(同向,能接上)的电梯,然后比较他们的“时间距离”,把当前任务分给时间距离最近的乘客。如果没有任何满足一层的,进入第二层,寻找没事干的电梯,找时间距离最近的,分配。如果还是没有,则进入了第三层,使用look算法循环分配至下一个能够接收任务且任务不重的。
设计中这三层分别对应的思想是,省电,省响应时间,不卡死。其实具体应用中也有所欠缺,比如如果全部reset在,然后来了人,这时候schedule会不停轮询,比较容易cpu超时等,其实可以加进一些sleep,在全部电梯都不满足三层的判断的时候歇一歇再看,会好不少。
本次作业bug依旧很多。
比如在一开始的设计中,电梯的ElevatorSetting与外部待处理人员队列是两个不同的对象,电梯在待处理人员队列为空的时候肯定不可能轮询访问,cpu会超时的,那就只能wait,但是这时候如果来了一个reset指令怎么办?要是你不处理,那它就会等到这个电梯被分配到接人任务的时候启动,人多倒是感觉不出来,人少或者直接最后一个reset,那就wait到天荒地老了。除非你在分配reset指令的时候,调用一下待处理队列的notifyall,把他拉出来,但是这样就非常的不优雅且臃肿了,还不如把他们俩合一起。
其次是很多的逻辑bug,还是电梯外部和内部的调度策略的问题,比如什么分配不对,还有所有电梯被reset时候的轮询超时,没有关注电梯当前的任务数,比如reset掉5个,这时候来一堆请求,如果没有关注任务数,调度器会把任务全给那一个能被分配的电梯,导致一核有难,五核围观的奇妙场景,直接超时。
最后也是最神奇的一个bug我觉得是,一定要注意电梯进入reset状态的互相共轭,比如,先改状态,再放人,放完人先把电梯状态置回正常,再回报。不然可能出现你把人out的同时把人塞回schedule的待处理队列的时候,电梯其实没有进reset状态,此时线程切换回了schedule,他又把这个人塞回来了,导致直接出错。这时候就电梯改了状态,再把列表塞回去,reset检测这个电梯就是不可被分配的了,就OK了。
UML类图如下:
UML协作图如下:
可以看到,这次作业基本上的改动明显的就是多了个DoubleElevatorInfo这一个类。
第三次作业相比于第二次多了的是支持双轿厢电梯,双轿厢电梯其实特征上也不知道是该说进行了简化还是复杂化(。因为实际上你能遇到的双轿厢电梯是俩绑一起的,或者干脆两个应该能随便跑。但是本次作业简化至了只会在设定的换乘楼层两边进行运作,这样减轻了很大的设计负担。
实际设计起来,每个双轿厢电梯可以看作两个独立的电梯个体,各自在各自的限制范围进行运行。需要进行的工作依次是,提供新的reset指令的支持,支持一个新的换乘楼层的互斥策略,防止俩撞一起,以及schedule一个新的分配策略。
因此,在这里设计了DoubleElevatorInfo这个类,这个类虽然看起来是个存储类,但是实际上存储的使用场景很少,更多的是作为换乘楼层的token,只有拿到了这个token才能够进入换乘楼层进行操作。这个类,会在AB两个电梯之间进行共享,在需要进入换乘楼层的时候电梯会尝试获取这个token,拿不到就只能等待了。
schedule类设计的准则与第二次作业基本没有太大差别,仍为三层分配check策略,只是需要多加一些限制,比如不能把一个A或者B电梯接不到的人,或者没必要接到(比如换成楼层为8,8楼到9的人,肯定不可能分配给下层的电梯)。基本就这些限制。
本次作业新增的同步块,基本只有DoubleElevatorInfo这一个token。整个设计也非常地暴力,你想要进入换乘楼层,就拿这key,拿到就进去干事,干完了退出来就把token释放掉。如果不需要进去,就不进去不拿了。整个换乘的楼层的业务单独为一个了,拿token,进,放人,出,释放token,基本全部单独构造,以便不与正常流程发生冲突,只需要在完成以后,将他加入正常的状态机流程开始循环就好了。
具体实现就像下面这样子。

本次作业的调度策略和第二次作业基本一样,具体的新增为对换乘电梯的分配限制,主体结构与思想并没有太大的变化。
先期bug主要出现在调度器上面,因为没有分析清楚能够进行分配的情况,产生了很多奇怪的分配情况,导致了部分电梯的卡死。后期也出现了第二次作业中,因为没有输出与操作共轭上导致的分配error。强测中的bug一个是出现在在换乘楼层放完人以后没有重置内部人数导致出错,这个比较好修。另一个问题就比较大了,为CPU超时,但是程序中观察后也没有出现循环等待等情况,就非常奇怪,也尝试过简化调度器策略,将全互斥锁更换为读写锁等优化,但是截止周六仍未找到问题所在与解决方法,只能先悬置了。
按照作业要求与个人的设计思路,我确实觉得第三次作业的设计思路与可扩展性可以单独提出来说一下。
先说一下前两次作业打下的一个比较好的基础吧,电梯状态使用了一个状态机,电梯会在Consult,move,arrive,open,inout,close,reset这几个状态中间进行循环,如果需要出现全新的状态,就可以直接在状态机里面加入新的状态,能转的通就好了。其次是部分状态可以进行复用,毕竟电梯也不会变太离谱。
第三次作业针对双轿厢电梯,算是偷了点懒,并没有直接在电梯从单轿厢的时候new一个线程出来,而是在一开始就布置好了12个线程,使用ElevatorType这个位于ElevatorSettings的标志位来表示电梯种类与情况,AB分别对应双轿厢的AB轿厢,C为正常全功能电梯,D为没有被启用的电梯。说白了,一开始就为他们设置成了“双子电梯”,只是有一个没有启用罢了。两个之间靠DoubleElevatorInfo这个token进行联系,同时使用了这个作为token,也可以互相找得到(其实并没有怎么用互相通信这个功能)。在reset为双轿厢的指令来的时候,这个指令只会被送往状态为C的全功能电梯,由他把自己重置完了以后,通过DoubleElevatorInfo这个记录的信息找到自己一直歇着的兄弟电梯,把它唤醒设置好了开始工作。同时电梯内部也可以根据这个type进行不同的业务判断,从而选择不同的运行流程。
至于最重要的“防撞”功能,只要保证安全的流程就行,具体便是“有进有出”,没事别在换成楼层停就好了,直接为它写一个临界区策略就好了。
最后是可扩展性。状态机的使用使得其能够很好地进行扩展,只要保证状态机能正常转起来就好了,同时状态机的状态标量,也能够非常便于外界观察电梯当前状态,只要你同步的够及时,正确地上了锁。Settings这个类的设置,其实很好的抽象了电梯,把具体的电梯进一步抽象成了一个回执,可以对外暴露更多电梯的细致信息,方便调度器进行决策,未来有新的参数或者策略,也可以直接这个使用。
可能可扩展性最大的缺憾,就是电梯内部的调度策略并没有进行很好的封装,直接在电梯类内部去完成了,要是能封装成根据不同电梯类型调出来的类就好了。但是缺点就是例如settings和乘员类会被传来传去的了。
本单元三次作业其实难度梯度挺平和的,也确实有助于构建一个足够健壮的电梯系统。特别是第一次对电梯内部的的单独构造,能够便于电梯自身的调度具有较好的鲁棒性,即使调度器没有能够分配给这个电梯“最适合”的请求,它也不至于不分三七二十一地接上然后导致效率降低或者错误,就挺好的。
本单元主要针对的是线程安全,其实线程安全最重要的并不是一个激进地共享与调用策略,反而是在能力所及范围内的“最保守”的设计策略。你不能拿程序的安全性为代价冒险进行性能的提高,反而应该优先保证安全性,因为bug的存在只有存在和不存在两种情况。在保证了安全性的情况下,通过降低上锁粒度,提高读读可容性,降低无效地锁持有时间来进一步提高程序运行效率。以上这些也是我本单元设计时的准则,如果不安全,宁愿它不存在。所以实际上也没有产生类似于死锁的安全事故,更多地是业务逻辑方面的错误。
bug分析与处理方面,其实主要的思想在于分层的bug分析的思想。首先,肯定处理的是线程不安全的bug,比如上锁异常这类,这类也是最不好分析的,建议使用System out输出或者用log输出的方式,也可以利用插件直观的观察锁的能够访问者的情况。线程安全bug处理完了以后,建议的是直接去d schedule类的bug,因为它只有一个线程,相对来说更加独立,同时分配时候出现的问题,往往是影响最大的,保证它的正确性是最优先的。最后才是去查找电梯类内部自治的问题。这里有最后一个小技巧,在保证前面两个内容正确的情况下,可以单独对电梯dbug了,通过把出问题的电梯的分配请求全部摘出来,然后按顺序和时间情况重新塞给电梯,基本可以稳定地复现遇到的bug。这时候其实只用启动一部电梯,保证分配的稳定性。这中分层dbug与第一单元按流程dbug是不太一样的。
本次作业的测试情况其实仍不容乐观,但是真的已经尽力的,相对第一单元也有了很大的进步,希望后面作业能更加顺利吧。