301
社区成员
发帖
与我相关
我的任务
分享 第五次作业是让我们实现一个能正确接送人的简易多电梯系统,需要我们掌握好多线程程序的写法。对于电梯调度策略,我用了LOOK算法,让电梯只接与电梯同向的请求,在电梯为空时寻找剩余的请求,若同向无请求则反向寻找请求,这个策略比较合理,也让我在第一次作业中得到了较高的性能分。

在hw5中我选用了InputThread,Schedule,Elevator三类线程的设计,其中InputThread是输入线程,可将输入的请求加入总队列,Schedule调度器线程为电梯分配了请求,提高了程序的可扩展性,Elevator线程模拟了六部电梯。
第六次作业的乘客请求未安排电梯,需要我们自己写一个调度器,且添加了可重置电梯速度和承重的reset请求。

这次作业新增了Reset类用来记录reset信息,对于reset的处理方式,我选择将reset视为一种请求,在电梯类调用getAdvice()之前检查队列中是否有reset请求并优先处理,让电梯就近停靠,将电梯中的人清空并放入该电梯队列中,在reset结束后重新输出receive,且正在进行reset的电梯的condition会被置成1,此时不会往此电梯对列加入新的请求。
对于调度器,我只是优先将请求放入请求数最少的队列中,并没有做其他的处理,这也使我的性能分较低。
第七次作业添加了可把电梯变为双轿厢电梯的功能,并给出换乘楼层,两电梯不能同时在同一层。

在hw7中,对于可以把电梯变为双轿厢电梯的reset,我将它与普通的reset看成一类处理,在一开始就新增加了6个电梯线程备用,用lowfloorh和highfloor代表电梯可到达的最低楼层和最高楼层,在调度时保证将请求放入起始楼层在最高最低到达楼层之间的电梯队列,初始化时可通过将6个备用电梯的最高楼层设为-1来防止电梯提前运行。其余双厢电梯和普通电梯的运行逻辑区别都在Elevator中实现了。
增加了ElevatorQueue和Occupy类,其中ElevatorQueue就是电梯的队列,方便在Elevator中调取、改变其他电梯。Occupy类可防止双娇厢的俩个轿厢相撞,是双轿厢AB电梯线程的共享对象,需加锁。
由于我的hw7沿用了hw6的调度策略,所以性能分仍较低。
为了实现双轿厢电梯两个轿厢不相撞,我参考了讨论区同学的方法,在Occupy类中设了OCCUPIED和UNOCCUPIED俩个状态,A,B电梯共用一个Occupy变量,在电梯到达换成楼层时检查Occupy状态,若为OCCUPIED则需wait(),若为UNOCCUPIED则执行move并把Occupy状态置为OCCUPIED,在离开换乘楼层后再把状态改回UNOCCUPIED。为了线程安全,Occupy类中的所有方法均需要上锁。
为了确保双轿厢不相撞还需要让在换乘楼层的电梯尽快离开换乘楼层,我让电梯执行close()关门之后,若发现此时在换乘楼层,则立刻移动到旁边的楼层。

在迭代时,InputThread,MainClass,Advice,Person等类比较稳定,主要原因是因为电梯算法一直用的是LOOK算法,三类线程的结构也与后续的扩展相契合,提前在hw5设好了size等参数也减少了后面的工作量,由于我将hw6和hw7中的reset都视为了一种特殊的请求,所以输入线程的改动不大,可扩展性高。Elevator类改动最大,因为我将后续Reset的处理和双轿厢电梯的新增功能都放入了Elevator类中,其实可以新建一个继承Elevator类的双轿厢电梯类来减少Elevator的复杂度和代码量,提高可扩展性。
根据UML类图可以发现,三类线程的结构可扩展性较高,对于新的要求只需要改变电梯的行为,在迭代时不需要改整体结构,每个模块的功能集中,便于扩展。
在hw5中,共享对象只有RequestQueue类,于是我将RequestQueue类的所有方法都上了锁。hw6同hw5。
在hw7中,我增加的Occupy类是AB电梯线程的共享对象,对其中的三个方法都需加锁。因为双厢电梯需要等另一个电梯无请求且电梯内无人的时候再让线程结束,所以我在Elevator类中新加了两个上锁的方法setEleempty和isEleempty来判断电梯是否为空。
这三次作业我均选择了用synchronized来给方法上锁,并用notifyAll()释放,在线程需要wait的时候加入了同步块。另外一定要减少 continue等语句的使用,尽量用wait()替代,避免ctle。
在这单元的强测中,我未出bug,但在hw6被hack了很多次,原因是因为我不会往正在reset的电梯里加请求,所以在同时reset五个电梯时会把同时间所有的请求都扔到剩下的那一个电梯里面,导致超时。其实我的程序会把reset后的电梯队列里的所有请求重新receieve,所以我实际上是可以往reset的电梯里面加请求的,只要不输出receive即可,但可惜由于我想的架构不太清晰,又小重构了一下,忽略了这一点。
多线程程序非常难debug,无法同时看多个线程的结果,我总结了几条经验:
电梯单元真的是令人印象深刻,hw6和hw7几度令我崩溃,层出不穷的bug让我放弃了很多更好的调度策略,最终后两次作业的调度方法都非常简朴,性能分较低。其中我遇到最大的困难是线程安全问题,随着作业要求越来越多,我时常发现线程会莫名其妙终止或者干脆就永远不停,有时还会因为读错其他线程的数据出现bug,最后不断改变线程的终止条件才解决了问题。
对于线程安全,首先要想明白数据竞争可能会出现在哪里,可以通过调整策略和架构使需要加锁的地方减少,降低线程安全出问题的风险,上锁后一定要记得notifyAll(),避免死锁。其次要想好各个线程的终止条件是什么,在写hw7的过程中我的双轿厢电梯线程会因为为判断另一个电梯是否结束而提前终止,导致错误。
多线程的程序更考验我们层次化设计的能力,在写代码前,最先要想到需要分几个线程,这样分是否有利于后续迭代,不能因为一周的轻松导致后面几周大幅度修改代码。尽可能让每个类每个方法都只负责一个功能,如Advice类只为电梯提供策略等,这点我在这单元做得比上单元好了许多,没有出现复杂度过高的类。在大体确定下架构之后,一定要预先设想各种情况,验证自己想的架构是否能处理这些特殊情况还能保证不出错不超时,这样能减轻后面debug的工作,也能避免重构。我在hw6时就是因为没有事先充分考虑所有情况,想得过于简单,最后小小重构了一下。