多线程电梯调度 总结分析

22371328曹玮琳 2024-04-20 19:39:29

面向对象设计与构造第二单元--电梯调度 总结分析

代码结构

首先展示代码的主要结构。

 左边展示了线程,总共三类线程InputThread,Assignment,Process。右侧展示了存放request的List。InputThread将信息读入并放入WaitList之中,Assignment从WaitList读取request并将其分配到SubList之中,之所以多加入了SubList类是因为WaitList中的方法和SubList之中的方法有很大的差别,因此分开设计不将其全部堆到一个类里。Process获取SubList之中的信息,发出指令指导Elevator的下一步动作。Elevator也需要SubList,因为其需要调用SubList中的方法删除进入电梯的乘客请求,还需要将中途退出的乘客重新加入到SubList之中。此外,重置时,SubList中的全部请求加回到WaitList。

互斥与同步

  • 对于存在共享的对象使用了锁,即WaitList,SubList,以及为双轿厢电梯上锁设置的Object。

  • WaitList的锁和训练代码中的完全一致,即isEnd,setEnd,isEmpty,getOneRequestAndRemove,addRequest方法上锁。

  • SubList和WaitList有所不同,其不能通过一次得到一个用户请求来实现,因此新方法getList采用了浅拷贝,但是无疑也需要上锁,此外还有将全部请求移除并重新加入到WaitList的方法,还有单个移除的方法,这些都涉及到访问的冲突,因此需要上锁,这里因为方法普遍不长而且大量涉及到对List的访问,因此直接对整个方法加锁。

  • Assignment需要得到Process的信息,但是这里仅仅是为了方便调度,并且也并不一定需要知道此时Process是否在重置,只需要保证不在重置时输出receive,这可以通过很多方式实现,我采用了不在调度器中输出,而是在Process中每次循环开始更新列表时输出,这样直接保证了不可能在reset时(这在循环的后续)输出receive(可能有点取巧了,现实中这意义不大)。因为需要获得此时SubList和Elevator人数之和,但是这仅仅为了方便调度,就算因为冲突和预计的值略有出入,也没有多大影响。所以Assignment获取Process信息没有加锁。

  • 双轿厢电梯不能同时到达换成楼层,因此令其共享一object对象,加锁的范围为从输出Arrive-换成楼层开始到输出Arrive-非换乘楼层为止,这一过程是强制连续的。这样可以做到,前一个电梯刚到达非换成楼层,下一个电梯就立刻输出到达换成楼层,最大程度的节省了时间,并且不会存在下一个电梯运行过快等问题。

调度策略

  • 运行策略:为look的加强版,即先按look策略上行下行,直到当前方向没有更多请求为止,但是如果当前方向因为人数限制,有未得到处理的请求,则将其加入remain之中,然后在这一趟运行结束后回头接上,而不是直接开始进入下行状态,这在应对大量的请求时,有利于减少运行的耗电量,平均等待时间以及最长等待时间。这需要设置一Maindirection,用于在进行相反方向的运行时明确主要的方向,从而实现了接remain中的请求而不是直接开始下行。

  • 捎带策略:一般当请求与当前实际运行方向同向时捎带。并且,对于“暂时的回头”,即direction与Maindirection相反时,如果请求的目的地不超出所到达的最远位置则将其加入,否则不加入,以免这个人一直霸占电梯不下,或者影响当前任务执行效率,成为额外负担。

  • 分配策略为:一个很基础的想法,即优先将乘客加入到当前运行方向和请求方向一致的电梯便于捎带,而如果当前运行方向的电梯人数比反向的多2,则将其加入反向电梯,以免造成大量的请求加入同一个电梯之中。在此基础上,排序得到SubList + insideList总人数最少的电梯(也即未开始执行的任务和已经在电梯中的任务总和最少)加入。对于双轿厢电梯,将其也加入调度俩表与其他的同样处理,双轿厢电梯共享一个SubList,其中一个电梯未完成的任务只能由另一个电梯完成。这一方法经实践检验,非常的差劲,原因可能是双轿厢电梯并没有承担足够的任务量,且一个任务必须由双轿厢的两个成员完成,且请求方向与运行方向一致并不代表可以成功捎带,还有可能已经越过了而需要下一轮才能捎带,且人数均摊大大增加了耗电量等等方面的原因导致,综合来看这一调度策略不仅复杂而且做负功,甚至还不如random,下次不会再一厢情愿的自我感动当小丑了,能简单实现的不求复杂,尤其在复杂却未必有用的情况下。

架构变化

类依赖关系变化

  • 第五次作业的Assignment不需要得到Process的任何信息,SubList也不需要向WaitList返回任何未完成的请求,因此少了两个依赖关系。后续则如上面的图所示。

方法调用变化

  • 第五次作业的Process的run方法如下图:

     

    这里电梯如同一个有限状态机,Process先判断是否结束,然后更新列表的内容,最后给电梯发出移动的指令,电梯的移动方法是一系列动作的集成,首先判断本楼层是否有需要进出的乘客,如果有则开门,先判断是否有本层下的乘客,再判断是否有本层上的(这里保证不超员)。先下后上,有序乘梯。最后如果开门了则关门,然后电梯根据当前设置的方向用0.4s时间运行。 

  •  

    第六次作业run方法如下

     

    可以看到在update中调用getList更新的时候会根据上一次的List和这一次的List得到新加入的乘客并输出receive,这保证了不会出现reset时receive或者少输出receive多输出receive的情况。多加入了tryReset的方法,其根据resetList是否为空做出是否进行reset的判断,reset先开门(如有必要),将内部的全部乘客放出再关门,将全部的请求加会SubList,再一并删除,加入WaitList重新调度。然后开始重置,输出RESET-BEGIN,然后将参数重置,等待1.2s,输出RESET-END。 

  •  第七次作业的run方法如下:

     update的getList中新加入了belong方法,即双轿厢的电梯用于判断当前指令是否属于自己可见范围,这一设置的用途后文再说。新加入了doubleCarReset,先将旧Process从Assignment的ProcessInfo中删除,再将两个新Process加入,再将旧的电梯中所有人清空加回,再将新线程开启。又加入了forceLeave方法,用于令在换成楼层的双轿厢电梯强制转移。

面向对象程度分析

这里按照惯例,先展示计算得到的结果。

这是Process中的方法复杂度图。

 这是Elevator中的方法复杂度图。

 这是SubList中的方法复杂度图。

 这相较于第一次作业大大改善了。没有过于复杂的方法,究其原因是因为将代码块恰当的抽象成函数,提高了可读性。

这是类之间的依赖关系分析。 也是很健康的,但是Process和Elevator之间的耦合度很高,这是难以避免的,因为Process既需要从Elevator获取信息,又需要指导Elevator的动作。但是虽然如此我还是选择了将二者分开,因为Process主要负责信息的分析,运行策略的计算,负责发出指令,可以看作是SubList和Elevator之间的中间层。而Elevator有自己行为,主要是move(),其中包含了open,out,in,close。还有reset,dcReset,有自己的位置ElevatorPos,方向direction,内部成员列表Inside。这都是属于电梯的行为,而计算,分析,发出指令则是Process的行为,我个人认为将二者分开,即使它们的耦合度很高,是有必要的。这里Process继承了线程类,Elevator没有,仅仅受Process的摆布,没有自主决策能力,不是AI-Elevator类。

双轿厢处理

  • 对于如何使得两个轿厢不会碰撞的分析见上文。

  • 轿厢共用一个SubList,这里出现了一些必须注意的问题,比如,如何使得电梯A和B的运行看起来像是一个正常的,仅仅是最高最低楼层不是1和11的电梯,如何使其不越过换乘楼层,使其保持look策略呢。我的解决方式是,对SubList的信息设置可见判断,将其分为电梯A可见和电梯B可见的部分,每个Process在getList时仅得到属于自己的部分,且在到达换成楼层时继续向上的必须出。这样一来不需要对运行策略进行改动,就可以实现其永不越界。

  • 电梯结束的条件除了SubList空且结束,自己的乘客全部送完,还有,必须等其伙伴运行完(或不存在伙伴),否则其伙伴产生的未完成请求可能永远得不到完成,产生错误。这里在doubleCarReset时对其设置了buddy伙伴电梯。

bug处理

  • 多线程出现过非常多的bug。

  • 首先是关于锁的,出现过本该notifyAll的选择了nofity导致有时出现死锁。

  • concurrentModificationError因为拷贝HashMap<ArrayList>的时候直接将ArrayList拷贝过来相当于ArrayList没拷贝直接传了。

  • 六个电梯同时重置时发生死锁,因为锁嵌套的太多了。

  • 至今不知道原因的电梯未receive就动了,后来改了在update中输出receive就不再发生了。

  • 双轿厢未等待伙伴结束就提前结束了导致任务未完成。

  • 双轿厢共享SubList本该wait时出现了不该有的来自伙伴的notify而轮询。

  • 总而言之多线程bug真的很多而且更难发现,主要因为无法进行调试,根本看不到实际运行过程,只能采用print大法间接看到执行的流程加以分析以及不断的读代码。这里感谢同学写的评测机,否则真的不知道会有多少bug。

心得体会

  • 这次设计的心得体会,前面说的差不多了,想补充的就是,复杂的策略未必真的可以达到想要的效果,而且非常容易产生更多bug,复杂的策略不过是为了适应自己设想的特殊情况,却很可能并不具有普适性,最后一次作业尽量简化实现,就很有利于bug的减少,但是手欠设计了复杂的调度策略,大概撞了枪口,性能分直接裂开,简洁有效优雅才是设计的最好原则。

...全文
98 回复 打赏 收藏 转发到动态 举报
写回复
用AI写文章
回复
切换为时间正序
请发表友善的回复…
发表回复

301

社区成员

发帖
与我相关
我的任务
社区描述
2023年北航面向对象设计与构造
学习 高校
社区管理员
  • YannaZhang
  • CajZella
  • C_ecelia
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告
暂无公告

试试用AI创作助手写篇文章吧