北航OO第二单元总结
作者:互联网
第二单元(模拟电梯)总结博客
第二单元要求模拟一部或多部电梯搭载乘客的过程。乘客可以在任何时间、任何楼层到达,并提供目标楼层。我们的程序主要功能是:要能够获取乘客的需求,并使用一定的策略将乘客运送到目标楼层。除乘客请求以外,还可能会输入电梯的增加请求,程序需要识别电梯请求,并增加相应电梯运行。
一、第五次作业
第五次作业中只有一部电梯,电梯到达层为1-20层。乘客有三种到达模式:Morning,Night,Random。
1. 锁和同步块
由于只有一部电梯,不需要对请求进行分类和调度,所以这一次作业我没有单独做出一个调度器(或者说调度器太简单,直接融合在了输入线程中),只有两个线程elevator
和inputHandler
,它们之间的共享资源只有一个:乘客的等待队列requestlist
。换句话说,这一次作业大体上是单生产者-单消费者模式:inputHandler
作为生产者,产生request存入requestlist
;elevator
作为消费者,对requestlist
进行分析,以特定顺序从中取出request。两个线程是“写-读”和“写-写”关系,都需要对requestlist
进行加锁。
这里有一点需要说明:由于乘客有三种到达模式,我使用了策略模式来对代码进行优化。在电梯中有一个
strategy
属性,每次程序执行时根据输入的到达模式设置电梯的策略对象。电梯的目标楼层的分析、乘客的电梯出入方法都由电梯策略类中相应的方法完成。但为了描述起来简洁,之后认为策略类的方法即是电梯类的方法。
具体同步块如下:
public class InputHandler extends Thread {
/*...*/
// 生产者产生request,锁住“盘子”
synchronized (requestList) {
if (personRequest == null) {
break;
} else {
requestList.addRequest(personRequest); // 放入request
requestList.notifyAll(); // 唤醒
}
}
/*...*/
synchronized (requestList) {
requestList.notifyAll(); // 唤醒,保证程序能正常退出
}
/*...*/
}
public class Elevator extends Thread {
/*...*/
public void peopleIn() { // 这里是其中一个策略类中的乘客上电梯的方法
// 消费者消耗request,锁住“盘子”
synchronized (requestList) {
ArrayList<PersonRequest> tmp = requestList.floorRequests(elevator.getFloor());
/*...*/
for (PersonRequest p : tmp) {
/*...*/
elevator.personIn(p);
requestList.removeRequest(p);
}
}
}
/*...*/
}
从上述的同步块中可以发现,当要对共享资源进行操作(包括读、写)时,就要对共享资源进行加锁。这样使得这一线程在访问共享资源时,阻塞其他线程对该资源的访问,从而保证操作的正确性。此外,对于wait()
、notifyAll()
这样的monitor命令,必须要在同步块中才能使用。
2. 调度器设计
这次作业中,由于只有一部电梯,且不会增加电梯,所有的乘客请求都由同一部电梯执行。所以对于调度器来说,工作非常简单:只用从生产者inputHandler
得到request,直接放入消费者elevator
即可。由于实现非常简单,我并没有单独对调度器进行实现,而是融合在了生产者的线程中(虽然这很不面向对象)。
对于电梯自己的调度策略,我使用了指导书中提及的ALS算法。这个算法完成了一些简单的捎带,显然不是最优的捎带算法。但是第一次接触多线程编程,我连程序基本的输入输出、基本架构的构建都还云里雾里,并没有太多时间来对性能进行很好的优化。考虑到正确性和架构设计是作业的重点,我选择放弃性能分。效果我也很满意,程序基本没有出现bug。
3. 程序bug分析
这一次作业是第一次接触多线程编程,程序容易出现bug的地方自然是线程之间的协作问题。在这一次作业中,synchronized
块的使用是比较难以理解的地方,由于是第一次接触,我选择了将所有对共享资源requestlist
的访问都进行加锁的方法。尽管这样可能会对性能有所影响,但这样保证了我操作的正确性,即没有出现request处理了却没有消除、缺失request等线程同步性安全问题。
但第一次作业中出现了一个隐蔽的问题。一般来说,当requestlist
为空时,inputHandler
和elevator
都会进入等待的状态。在我自己进行调试时,请求输入完毕后就直接输入^D表示输入结束,使得大多数情况下程序都能正常结束。但后来发现,一些情况下输入输出均结束了,程序仍然在运行。原因是我的代码中,elevator
结束的标志是inputHandler
结束且等待队列requestlist
为空,如果前者不成立而后者成立,elevator
会进行等待(requestlist.wait()
)。但在某些情况下,elevator
等待时,inputHandler
结束了却没有notifyelevator
(因为代码中只在有request时才notify),于是elevator
会一直保持wait的状态,程序无法结束。
对于互测他人bug的分析,我并没有对别人进行hack。原因是我造不出评测机,而且多线程代码非常难以阅读。估计大多数人和我的想法一样,这次互测room内没有任何一个人进行了hack。
二、第六次作业
这次作业,电梯数量从一个变为多个,且增加了“增加电梯数量”的请求。
1. 锁和同步块
这一次作业由于比上一次作业在结构上有了质的改变,所以共享资源、线程也发生了变化,锁和同步块也发生了变化。由于加入了调度器,以及电梯数量增加,线程数从两个直接跃升到5-7个,线程类别从两类变为三类。共享的资源(变量)也从原本的一个requestlist
变为一个waitlist
和若干个elevatorQueue
。
-
waitList
这个变量是乘客的总队列,输入线程
InputHandler
和调度器线程scheduler
共享。输入线程将得到的乘客需求放入waitlist
,调度器从中获取请求进行调度。两个线程对其访问都需要加锁,同时输入线程放入需求时需要notify,调度器在无法获取请求时需要wait。 -
elevatorQueues
这个变量是一个装有所有电梯乘客队列的ArrayList,输入线程
InputHandler
和调度器线程scheduler
共享。当输入产生新电梯的请求时,InputHandler
需要根据请求增加电梯,同时也要为新电梯增加乘客队列。而scheduler
需要从elevatorQueues
从寻找合适的乘客队列,以分配请求。两个线程对其访问都需要加锁。 -
elevatorQueue
这个变量是各个电梯的乘客分队列,调度器
scheduler
和各个elevator
共享。调度器将得到的请求放入特定的电梯乘客队列,电梯从自己拥有的电梯乘客队列取出乘客请求(即乘客进入)。这是一个一对多的情况,即调度器要与多个电梯交互。线程访问都需要加锁,同时调度器放入需求时需要notify,电梯在无法获取请求时需要wait。
2. 调度器设计
这次作业,由于电梯数量的增加,调度器成为一个必不可少的线程。由于在上一次作业中并没有对调度器进行单独构建,所以这一次作业也算是一个架构上的小重构。调度器需要连接输入线程和电梯线程,将“蛋糕”从生产者分配给消费者。这次作业的架构,我为每一个电梯分配了一个乘客等待队列,这样电梯只需要对自己等待队列中的乘客需求进行处理即可。而对于输入的不同乘客请求,调度器根据各个电梯队列、电梯状态来进行请求分配。
这一次的作业也有同学提出了“集中式调度”方法,说白了就是不直接通过调度器对请求进行分配,而是靠电梯“自由竞争”。由于我们最后的性能分是以程序最后结束的时间为评判标准的,而电梯“抢人”的算法虽然会让线程更加繁忙、输出变多,但总体时间似乎会更快?(老师说是因为调度器分配时只能保证分配是当前最优的,而自由竞争则是动态最优,但我并没有理解具体原因所以就不阐述了)不过,这样的算法对线程安全有了更高的要求,多个消费者“哄抢”可能会导致等待队列的同步问题,可能还会导致电梯空跑等待、缺失请求等其他问题。所以,同样出于正确性的考虑,我还是选择以调度器分配作为作业算法,并只做简单的优化:将乘客请求对电梯进行均分,即每次传入请求时都放入等待乘客最少的电梯。为了保证这一点,我对电梯(策略类)的peopleIn
方法进行了优化,使得只要电梯还没有离开当前楼层,都认为乘客还没有进入电梯,这样尽量地避免了乘客都全部集中到一部电梯的情况。
这里再提一点电梯策略的改进。上一次作业时,我基本上使用的是ALS算法,即先到先服务,以最早的请求作为主请求,其余请求做尽量捎带。根据第一次研讨学习的一些知识,我对策略进行了一些优化(主要参考了一点look算法):以电梯的运行方向为主,对等待队列中的请求进行分析,再决定电梯的目标。优化后电梯有变快,虽然不是特别明显,但也算是我对算法有一个优化的过程吧。
3. 程序bug分析
这次作业相对于上一次作业而言,在结构上有比较大的改动(因为自己上一次偷懒没有建立调度器),算是一次小重构。再加上为了满足均匀分配算法,调度器需要对电梯等待队列进行查询,与此同时电梯本身又需要进行乘客出入操作,这导致我需要对第一次作业本来建立好的同步块进行更改。可以想象到,由于我将电梯策略分为了三类,每个类进行方法的重写,本身工作量就不小。如此进行更新迭代,导致方法复杂度上升,这很可能导致bug的出现。
果不其然,这一次我出现了bug。似乎这一次的弱测和中测强度并不高,我的程序提交两次都能正确通过,让我错以为我的程序至少在正确性上没有问题。但强测结果告诉我:我的程序有一个巨大的bug,这导致我这次强测的分数只有60多,非常难受。这个bug是我在对乘客进入的方法优化时导致的,由于期望在乘客进入电梯时暂时不把乘客从等待队列取出,而是在电梯离开时取出,我将同步块和逻辑做了一点调整,但我却忘记将原本的return
更换为break
,导致乘客被多次运送,WA了四个点。除此之外,我也被强测中的高强度测试hack了,这个测试点从180s才开始输入乘客且大规模输入乘客,我的电梯调度算法似乎不足以让它们在有限的30s内完成任务,所以导致TLE。这是算法问题导致的,如果要修复只能对整个程序的架构和算法进行重构,这对于bug修复来说似乎太苛刻,所以我选择放弃修复这个bug。
这次与上次作业一样,没有hack别人,也没有别人hack。
三、第七次作业
这一次作业,仍然是多部电梯,但电梯规格不同(3种),包括电梯可搭乘人数、运行速度、可达楼层均有不同。
- A:1-20层,每层0.6s,8人
- B:奇数层,每层0.4s,6人
- C:1-3、18-20层,每层0.2s,4人
1. 锁和同步块
在整体架构和共享资源上,第三次作业和第二次作业没有很大差距,所以锁和同步块部分与第二次作业基本相同,这里就不再赘述。唯一的区别在于,由于电梯规格不同,elevatorQueue
也有三种类型,调度器要对三种队列区别对待,进行加锁。
2. 调度器设计
这一次的作业中,最大的改动在于,不是所有的电梯都能到达所有的楼层了。这给调度器带来了额外的工作:原来只需要暴力地平均分配乘客,现在则需要根据乘客请求的具体内容来进行分配。
这一次指导书上也明确地指出,乘客可以进行换乘。换句话说,理论上可以实现这样一种设计:电梯从原来的消费者转变为即是消费者也是生产者,总等待队列既从输入线程也从电梯存入请求,调度器对请求的起始楼层、目标楼层和可能存在的中转楼层进行分析,将请求放入相应类型的电梯进行处理,使得乘客到达目标楼层花费的时间最短。
经过分析,我发现换乘的实现并不容易。这其中牵涉到最复杂的问题在于,电梯的身份发生了变化,这不仅意味着电梯本身复杂性上升,还意味着整个程序架构需要产生变动。此外,人员请求需要发生改变,要对人员的中转进行分类,这看起来简单,但实际上“乘客”这个类与各个类的耦合度非常高(这是不可避免的),修改起来并不简单。同时,换乘还对线程安全产生了更大的挑战:动态增加的电梯要对共享资源waitlist
进行访问,这无疑增加了风险。而且,换乘时会面临额外的开关门代价,这意味着即使完成了换乘,如果完成得不够优秀,甚至可能导致性能倒退。
综合这些考虑,我决定暂时不实现换乘算法。那么调度器只需要进行一个优化:对请求的所在楼层和目标楼层进行分析,为其选择一种最快的可到达电梯,再在这种电梯中分配一部适合的电梯即可。由于不实现换乘,选择电梯类型时,只用以C、B、A的顺序检测乘客可否搭乘,然后分配对应的电梯即可。后者与上次作业同样实现,前者只需要一个简单的函数进行判断即可(电梯的到达楼层是静态的)。
3. 程序bug分析
由于不增加换乘的功能,程序整体上正确性和安全性由上次的作业进行了保证,增加的优化进行测试后也没有出现问题,这次没有出现bug。也没有hack别人。
4. 架构可扩展性
-
UML类图
-
UML顺序图
-
扩展性讨论
基本的程序架构如上。第三次作业由于是最后一次作业,所以在构建时对扩展性没有进行太多的思考,这是不应该的。在整体上,如果需要增加电梯类型(比如电梯的停靠楼层、运行速度、人员限制等),我的程序可以比较好的完成。但由于没有实现换乘,如果现在的A类电梯不复存在(比如换成只能停靠偶数层),那么程序将面临一个比较大的重构。此外,由于当时不想增加电梯类的复杂性,并没有对不同类型电梯进行一个统一父类-子类继承的管理,而是简单地在电梯类中增加了一个type属性。这导致如果要动态增加电梯类型,程序也会面临一个小重构。整体而言,第三次作业的程序可扩展性一般。
四、心得体会
电梯,这个名词从大一时就已经从学长学姐中听说,这一次终于揭开了它神秘的面纱。从刚开始的不理解什么是线程、不知道怎么输入输出,到后来保证不会出现线程安全问题,思考如何提高性能,我多线程的能力有了很大的提升。
线程弄明白是什么之后,就是弄明白线程之间怎么进行协作、怎么避免死锁等线程安全问题,再然后就是线程级别的层次分析,最后是电梯这个问题的调度策略。这些内容越往前越重要越基础,越往后越困难越深入,一个个地击破,便是这一个月来我的经历。
标签:OO,请求,乘客,作业,北航,调度,电梯,线程,单元 来源: https://www.cnblogs.com/exe-mao/p/14702140.html