其他分享
首页 > 其他分享> > 北航OO第二单元总结

北航OO第二单元总结

作者:互联网

第二单元(模拟电梯)总结博客

第二单元要求模拟一部或多部电梯搭载乘客的过程。乘客可以在任何时间、任何楼层到达,并提供目标楼层。我们的程序主要功能是:要能够获取乘客的需求,并使用一定的策略将乘客运送到目标楼层。除乘客请求以外,还可能会输入电梯的增加请求,程序需要识别电梯请求,并增加相应电梯运行。

一、第五次作业

第五次作业中只有一部电梯,电梯到达层为1-20层。乘客有三种到达模式:Morning,Night,Random。

1. 锁和同步块

由于只有一部电梯,不需要对请求进行分类和调度,所以这一次作业我没有单独做出一个调度器(或者说调度器太简单,直接融合在了输入线程中),只有两个线程elevatorinputHandler,它们之间的共享资源只有一个:乘客的等待队列requestlist。换句话说,这一次作业大体上是单生产者-单消费者模式:inputHandler作为生产者,产生request存入requestlistelevator作为消费者,对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为空时,inputHandlerelevator都会进入等待的状态。在我自己进行调试时,请求输入完毕后就直接输入^D表示输入结束,使得大多数情况下程序都能正常结束。但后来发现,一些情况下输入输出均结束了,程序仍然在运行。原因是我的代码中,elevator结束的标志是inputHandler结束且等待队列requestlist为空,如果前者不成立而后者成立,elevator会进行等待(requestlist.wait())。但在某些情况下,elevator等待时,inputHandler结束了却没有notifyelevator(因为代码中只在有request时才notify),于是elevator会一直保持wait的状态,程序无法结束。

对于互测他人bug的分析,我并没有对别人进行hack。原因是我造不出评测机,而且多线程代码非常难以阅读。估计大多数人和我的想法一样,这次互测room内没有任何一个人进行了hack。

二、第六次作业

这次作业,电梯数量从一个变为多个,且增加了“增加电梯数量”的请求。

1. 锁和同步块

这一次作业由于比上一次作业在结构上有了质的改变,所以共享资源、线程也发生了变化,锁和同步块也发生了变化。由于加入了调度器,以及电梯数量增加,线程数从两个直接跃升到5-7个,线程类别从两类变为三类。共享的资源(变量)也从原本的一个requestlist变为一个waitlist和若干个elevatorQueue

2. 调度器设计

这次作业,由于电梯数量的增加,调度器成为一个必不可少的线程。由于在上一次作业中并没有对调度器进行单独构建,所以这一次作业也算是一个架构上的小重构。调度器需要连接输入线程和电梯线程,将“蛋糕”从生产者分配给消费者。这次作业的架构,我为每一个电梯分配了一个乘客等待队列,这样电梯只需要对自己等待队列中的乘客需求进行处理即可。而对于输入的不同乘客请求,调度器根据各个电梯队列、电梯状态来进行请求分配。

这一次的作业也有同学提出了“集中式调度”方法,说白了就是不直接通过调度器对请求进行分配,而是靠电梯“自由竞争”。由于我们最后的性能分是以程序最后结束的时间为评判标准的,而电梯“抢人”的算法虽然会让线程更加繁忙、输出变多,但总体时间似乎会更快?(老师说是因为调度器分配时只能保证分配是当前最优的,而自由竞争则是动态最优,但我并没有理解具体原因所以就不阐述了)不过,这样的算法对线程安全有了更高的要求,多个消费者“哄抢”可能会导致等待队列的同步问题,可能还会导致电梯空跑等待、缺失请求等其他问题。所以,同样出于正确性的考虑,我还是选择以调度器分配作为作业算法,并只做简单的优化:将乘客请求对电梯进行均分,即每次传入请求时都放入等待乘客最少的电梯。为了保证这一点,我对电梯(策略类)的peopleIn方法进行了优化,使得只要电梯还没有离开当前楼层,都认为乘客还没有进入电梯,这样尽量地避免了乘客都全部集中到一部电梯的情况。

这里再提一点电梯策略的改进。上一次作业时,我基本上使用的是ALS算法,即先到先服务,以最早的请求作为主请求,其余请求做尽量捎带。根据第一次研讨学习的一些知识,我对策略进行了一些优化(主要参考了一点look算法):以电梯的运行方向为主,对等待队列中的请求进行分析,再决定电梯的目标。优化后电梯有变快,虽然不是特别明显,但也算是我对算法有一个优化的过程吧。

3. 程序bug分析

这次作业相对于上一次作业而言,在结构上有比较大的改动(因为自己上一次偷懒没有建立调度器),算是一次小重构。再加上为了满足均匀分配算法,调度器需要对电梯等待队列进行查询,与此同时电梯本身又需要进行乘客出入操作,这导致我需要对第一次作业本来建立好的同步块进行更改。可以想象到,由于我将电梯策略分为了三类,每个类进行方法的重写,本身工作量就不小。如此进行更新迭代,导致方法复杂度上升,这很可能导致bug的出现。

果不其然,这一次我出现了bug。似乎这一次的弱测和中测强度并不高,我的程序提交两次都能正确通过,让我错以为我的程序至少在正确性上没有问题。但强测结果告诉我:我的程序有一个巨大的bug,这导致我这次强测的分数只有60多,非常难受。这个bug是我在对乘客进入的方法优化时导致的,由于期望在乘客进入电梯时暂时不把乘客从等待队列取出,而是在电梯离开时取出,我将同步块和逻辑做了一点调整,但我却忘记将原本的return更换为break,导致乘客被多次运送,WA了四个点。除此之外,我也被强测中的高强度测试hack了,这个测试点从180s才开始输入乘客且大规模输入乘客,我的电梯调度算法似乎不足以让它们在有限的30s内完成任务,所以导致TLE。这是算法问题导致的,如果要修复只能对整个程序的架构和算法进行重构,这对于bug修复来说似乎太苛刻,所以我选择放弃修复这个bug。

这次与上次作业一样,没有hack别人,也没有别人hack。

三、第七次作业

这一次作业,仍然是多部电梯,但电梯规格不同(3种),包括电梯可搭乘人数、运行速度、可达楼层均有不同。

1. 锁和同步块

在整体架构和共享资源上,第三次作业和第二次作业没有很大差距,所以锁和同步块部分与第二次作业基本相同,这里就不再赘述。唯一的区别在于,由于电梯规格不同,elevatorQueue也有三种类型,调度器要对三种队列区别对待,进行加锁。

2. 调度器设计

这一次的作业中,最大的改动在于,不是所有的电梯都能到达所有的楼层了。这给调度器带来了额外的工作:原来只需要暴力地平均分配乘客,现在则需要根据乘客请求的具体内容来进行分配。

这一次指导书上也明确地指出,乘客可以进行换乘。换句话说,理论上可以实现这样一种设计:电梯从原来的消费者转变为即是消费者也是生产者,总等待队列既从输入线程也从电梯存入请求,调度器对请求的起始楼层、目标楼层和可能存在的中转楼层进行分析,将请求放入相应类型的电梯进行处理,使得乘客到达目标楼层花费的时间最短。

经过分析,我发现换乘的实现并不容易。这其中牵涉到最复杂的问题在于,电梯的身份发生了变化,这不仅意味着电梯本身复杂性上升,还意味着整个程序架构需要产生变动。此外,人员请求需要发生改变,要对人员的中转进行分类,这看起来简单,但实际上“乘客”这个类与各个类的耦合度非常高(这是不可避免的),修改起来并不简单。同时,换乘还对线程安全产生了更大的挑战:动态增加的电梯要对共享资源waitlist进行访问,这无疑增加了风险。而且,换乘时会面临额外的开关门代价,这意味着即使完成了换乘,如果完成得不够优秀,甚至可能导致性能倒退。

综合这些考虑,我决定暂时不实现换乘算法。那么调度器只需要进行一个优化:对请求的所在楼层和目标楼层进行分析,为其选择一种最快的可到达电梯,再在这种电梯中分配一部适合的电梯即可。由于不实现换乘,选择电梯类型时,只用以C、B、A的顺序检测乘客可否搭乘,然后分配对应的电梯即可。后者与上次作业同样实现,前者只需要一个简单的函数进行判断即可(电梯的到达楼层是静态的)。

3. 程序bug分析

由于不增加换乘的功能,程序整体上正确性和安全性由上次的作业进行了保证,增加的优化进行测试后也没有出现问题,这次没有出现bug。也没有hack别人。

4. 架构可扩展性

四、心得体会

电梯,这个名词从大一时就已经从学长学姐中听说,这一次终于揭开了它神秘的面纱。从刚开始的不理解什么是线程、不知道怎么输入输出,到后来保证不会出现线程安全问题,思考如何提高性能,我多线程的能力有了很大的提升。

线程弄明白是什么之后,就是弄明白线程之间怎么进行协作、怎么避免死锁等线程安全问题,再然后就是线程级别的层次分析,最后是电梯这个问题的调度策略。这些内容越往前越重要越基础,越往后越困难越深入,一个个地击破,便是这一个月来我的经历。

标签:OO,请求,乘客,作业,北航,调度,电梯,线程,单元
来源: https://www.cnblogs.com/exe-mao/p/14702140.html