OO第二单元作业总结
第五次作业
1. 架构分析
第五次作业为简单的单电梯调度,限制很少,主要在于熟悉多线程的设计与使用。
在架构上,由于考虑到之后的迭代作业,我在第一次作业中就将电梯和调度器分离。lift类表示电梯的状态,liftThread电梯线程用来控制lift(后来经过思考觉得没有必要将电梯的状态单独分出来,于是在后两次作业中),conThread调度线程,reqThread请求线程负责添加请求。同时使用了LiftRunInfo类来做类似“托盘”的工作(但其实和消费者生产者间还是略有区别)。Request用来存储所有PersonRequest信息。(使用floorRequest分层进行存储)
在算法上,我将所有的request分配工作都交给了调度器。大致流程如下:
①. 控制器查看电梯是否停止,若未停止则wait等待电梯等待唤醒。电梯停止,并wait等待控制器唤醒。
②. 电梯停止,唤醒控制器,控制器查看请求队列,若当前楼层有请求,全部放入LiftRunInfo中(因为电梯没有容量上限),唤醒电梯,若当前楼层没有请求,则向上向下进行搜索,搜索到最近的有request楼层,将信息传至LiftRunInfo,唤醒电梯。若搜索不到request,则wait等到新的request进入或者查看结束符结束。
③. 电梯被唤醒,根据LiftRunInfo中的内容运行,将电梯内的人运往目的地。
在数据冲突上,这次数据冲突只有调度器读取请求队列和请求线程添加PersonRequest的冲突。(LiftRunInfo不会被调度器和电梯同时访问,调度器填装好LiftRunInfo后唤醒电梯线程,电梯才会对LiftRunInfo处理)
可以看出,controller中的方法使复杂度最高的,因为controller里聚集了所有与调度相关的函数。
2. Bug处理
第五次作业在互测和强测中没有出现bug,在互测中盲狙到了一位使用轮询导致CPU超时的同学。
3. 总结
这次作业在设计上由于过于害怕线程之间的数据冲突,于是“过分解耦”导致性能并不理想,只有94分。比如电梯线程对request list是一无所知的,所有的Request都是从LiftRunInfo这个“托盘”中获得的,但是捎带效果理想的话应该在运行过程中进行查找更有效率。在经过反思后,我在第六次和第七次作业对这种设计进行了修改。
第六次作业
1. 架构分析
这次作业增加了多电梯、负楼层和电梯容量上限。负楼层直接使用可逆映射函数映射到下标,电梯容量上限处理也比较简单,主要在于多电梯的配合问题。
在架构上,首先,我在这次作业里改掉了之前的调度算法,调度器只负责查找最近有PersonRequest的楼层并调度空电梯前往,而装填PersonRequest的工作(包括到达指定楼层后的第一次装填和运行时捎带)都由电梯自行完成(即电梯自己抢请求),电梯每运行到一层,若电梯没有满则捎带上相同方向的请求。这种捎带设计所带来的性能优化肉眼可见。
其次,对于多电梯的请求处理,我使用了waitlist处理的方法,这也是我个人认为比较有意思且实用的设计。controller并不直接控制每个电梯线程(否则容易导致某个电梯线程饿死的情况),而是只处理waitlist中的线程,处理完了之后便将线程从waitlist中移除,当waitlist为空时便wait。对于电梯线程,当电梯非空时只负责捎带加运送电梯内乘客至目的地,当电梯为空时wait并将电梯线程加入waitlist(ConThread中有一个public toWaitList()函数)。由于添加waitlist是添加到队尾,移除是从队头开始移除,因此这种处理方式也可以使多个电梯轮换得到充分利用。
在数据冲突上,这次数据冲突有调度器读取request list(查找最近楼层)、电梯线程读取floor request list(查找当前楼层是否有捎带)和reqthread添加request的冲突。事实上有的数据冲突不上锁不会影响正确性:
①. 查找请求队列后增加请求或减少请求(某个电梯已捎带),读后写,造成的结果只是将空电梯分配到该楼层后发现没有请求,继续等待,对正确性没有影响。
②. 在电梯处理捎带时添加请求,读后写。卡点上电梯,显然不存在问题。
但是有的数据冲突会影响正确性:
①. 多个电梯同时在同一层楼接收请求,可能会导致混乱,所以在电梯线程检查捎带时对该楼层的请求队列上锁。
②. 调度器查找最近有请求楼层时发现请求队列为空,而在查找结束即将wait时新的请求加入了,可能会造成无法唤醒调度器工作。因此在调度器查找最近有请求楼层时发现请求队列为空时带锁进行二次查找,避免保证上述情况发生。
③. 调度器发现waitlist为空时等待waitlist唤醒,而在判断waitlist为空即将wait时有电梯进入waitlist,可能会造成无法唤醒调度器工作。处理方法同上。
仍然controller中的方法使复杂度最高。
2. Bug处理
第六次作业在互测和强测中没有出现bug,在互测中也未能找到其他人的bug。在第七次作业迭代中发现第六次作业的添加请求和搜索最近请求楼层的死锁bug。(由于在这次作业中request加入时间过短导致很难复现,而在第七次作业中则稳定复现)
3. 总结
这次作业相比于第五次作业修改了调度算法,性能有所提升,强测99.2还算比较满意(卷不动了,走了走了)。
第七次作业
1. 架构分析
这次作业增加了动态添加电梯、换乘与电梯种类(不同电梯数据不同)。动态添加电梯简直完美契合waitlist设计,只需在请求线程中创建新电梯即可,不改。于是这次迭代重点在于换乘与电梯种类。
对于换乘,因为三种电梯都可以到达1层和15层,也就是说任何一个请求可以通过至多一次换乘到达目的地。我在第六次作业的基础上增加了PersonReqPlus类(字面意思PersonRequest升级版),将PersonRequest和换乘信息(换乘信息直接打表)封装至PersonReqPlus类,这样通过套一层壳子封装,可以使调度器清楚得了解该请求的实时信息,包括是否需要换乘、是否已换乘、换乘的楼层、可供换乘的电梯种类等。
除此之外,改动就比较小了:对每一个电梯type单独设一个控制线程,对每一个电梯线程,在初始化时标记可到达楼层,仅当到达可到达楼层时才可以上下客(电梯在接客时也会检查是否可到达)。这种做法本质上也是让不同type的控制器自己抢,因此没有充分利用“有的电梯运行时间较短”的可优化点。但实际上,如果想从这个点上进行优化,势必要知道各个电梯的运行位置来进行一个全局的计算,否则会出现“虽然电梯本身运行很快但是等待电梯就位却耗掉了更长时间”的情况。但进行全局的判断势必又会增加数据冒险,尤其是电梯还在动态运行,想通过电梯的数据进行计算优化而不出问题实在不算简单。因此进行权衡后我依然选择了这种调度算法,写起来不复杂且不会考虑细枝末节的东西,性能也不会差太多。
2. Bug处理
第七次作业在强测中没有出现bug,互测中被他人找出一个BUG,原因是在电梯换乘时电梯下客后和把该换乘乘客放到Request List前进行了检查,导致提前退出(这个时间段内电梯为空且request list为空,于是判断退出了线程。由于有锁,所以这段时间并不算太短,是不太难卡的)。互测盲狙找到了一个老哥的BUG也是在换乘的时候提前结束线程导致换乘乘客未能到达目的地。
3. 总结
由于强测纯随机导致没有卡到BUG,强测99.3,至此,二单元苟住。