BUAA_OO_U2_Summary

BUAA_OO_U2_Summary

由于本人废话比较多,所以提供一个目录

一 / 架构设计

1.0> 题目解析

  1. 有人要坐电梯

  2. 电梯要尽量快的把他们送到目的地

  3. 电梯有竖向的,也有环形的,环形电梯有可到达楼座的限制

1.1> HW5

1.1.1> 做法分析

我们现在有电梯和很多在一定时间和一定地点出现的有一定需求的乘客。需要做的就是在电梯不超载的前提下尽量快速地完成所有人的请求。

而根据训练所给出的两种设计模式的提示,本人首先利用了大量时间学习生产-消费者模式,并尝试思考出如何进行第五次作业的编码。这个过程历时过长,导致最后留给调试的时间不多,只是将将通过了中弱测,并在强测迎来了惨死,当然,这是后话了。

为了套用这个生产-消费者模式,首先需要解决的问题是谁是生产者谁又是消费者,他们在生产什么又在消费什么

一开始本人以为生产消费针对电梯上面的空位,人下电梯则空位是被生产,人上电梯则空位被消费。但是转念一想,发现了问题。如果人乘客电梯,那么乘客是消费者,电梯是生产者。但如果乘客下电梯了,电梯是消费者,乘客是生产者,角色并不固定,并不能利用此设计模式进行编码。很显然,对于这个被生产消费的物品,本人选错了。

那我们继续来观察整个电梯运行事件中出现又消失了的事件——乘客的需求。在这样的条件下,生产者固定,为到达电梯口想要从这里出发去别的楼层的乘客;消费者固定,为运输这个乘客到达目的地,满足其需求的电梯。所以,我们的代码需要做的事情就是在乘客输入请求后,把请求分配电梯,让电梯处理请求。这三件事,即整个代码中要用到的三个线程,串其这三条线程,贯穿于整个程序之中的,便是装有请求的托盘,而用于按时间戳递增的顺序输出信息的,便是线程安全输出类

1.1.2> 获取请求

读入线程。我们只需要把乘客的请求读入,放上托盘。

这个线程设计到的难点是,如何在输入停止后,通知另外两个线程没有新请求了,大家处理完手头的任务就可以下班跑路了一事,即线程结束的问题。这里的方法在训练的代码中有详细的指导,于是本人便毫不犹豫地将其搬了过来,大概思路为:如果当前读入表示没有新的请求,则将所有的电梯待处理队列的状态设为结束态。当电梯的处理队列为空且状态为结束态时,直接退出线程。

1.1.3> 请求分配

分配器线程。我们需要把乘客的请求分类,分发给不同的电梯处理。

在此次作业中,有5栋独立的楼,里面分别由一个电梯,于是这个分配器需要做的事情就是把每个请求扔到对应的楼座里的电梯那里。在当前这个每个电梯互相独立互不干扰的前提下,此线程的实现并没有什么难点和需要注意的地方。

1.1.4> 电梯调度

电梯线程。我们需要让电梯跑起来,去接每个乘客从自己的起始地点开始上电梯,到达自己的目的地,出电梯。

这里是本次作业中最难最难最难的部分,本人几乎所有的时间都利用在思考如何编写这个电梯的调度上了。毕竟,我可以让我的电梯每次指处理一个请求,然后享受RTLE的洗礼,并继续硬着头皮思考如何调度电梯。在当时较为绝望的境况下,本人并不能够理解ALS的调度方法中的主请求是如何被设置并在代码中实现这一点,也未能相处如何编写在大量往届学长学姐的博客中扑腾到的LOOK算法,但本人并没有放弃,而是利用了三天时间仔细的和自己讲述、辩论,并编写、调试。在此非常乐意记录一下自己的分析过程。

抽象来看,所有的调度不过是状态的转换。从乘客的请求方面,有未进电梯(请求等待)、进入电梯(处理请求中)和出电梯(完成请求)三个状态;从电梯的运行方面,有原地等待(等待请求),接人(获得请求),运行(处理请求),开/关门这四个状态。对这些状态如何互相转化进行设计,形成一个状态机,则电梯就可以完成基本的运作了。

《状态机》

而调度的关键在于接人的顺序,这也是所有算法着重处理的地方。

本人的想法很贪心,但是是没有被证明过的那种(应该可以证明有更优解但我并没有仔细思考),可能更加贴近LOOK一些:

  • 电梯在等待状态下:

    • 选择出发楼层距离1层和10层位置最近的请求(为了一趟多接一些人)

    • 满足上条的情况下,选择出发楼层距离电梯当前位置最近的请求

    • 满足上条的情况下,选择目的楼层距出发楼层最近的请求(为了一趟多接一些人)

    • 如果没有这样的请求,则继续等待

  • 电梯在上行状态下:

    • 选择移动方向为上行的请求

    • 满足上条的情况下,选择出发楼层不低于当前电梯所在楼层的请求

    • 满足上条的情况下,选择目的楼层距出发楼层最近的请求

    • 如果没有这样的请求,则继续上行,完成当前的请求

  • 电梯在下行的状态下:

    • 选择移动方向为下行的请求

    • 满足上条的情况下,选择出发楼层不高于当前电梯

    • 满足上条的情况下,选择目的楼层距出发楼层最近的请求

    • 如果没有这样的请求,则继续下行,完成当前的请求

但是,必须承认的一点是,其性能分比较糟糕。

但是,只有这样一个方案,对于最终形成代码依旧有一定困难:比如,代码在什么时候进行这个状态的检测并去询问有没有可以捎带的人呢?所以,更详细来说:

  • 电梯初始为等待状态

  • 按规则获取请求

  • 得到请求

    • 到达请求的起始层

    • 若电梯未满,按规则获取新的请求

    • 得到新的请求

      • 向着当前新的请求的起始层移动

      • 若电梯未满,按规则获取新的请求

      • 每次移动一层

      • 到达楼层的时候判断是否有请求要出电梯 / 进电梯

      • 后出后进,先下后上,弘扬中华民族的传统美德(为了保证不超人数)

    • 没有新的请求则直接处理当前请求

  • 沿当前方向继续按层运行,到电梯中所有人都出去为止

  • 电梯状态再次设为等待

上面的流程不断重复,直到获得了等待队列为空且没有新的请求了的信号,则退出线程,结束电梯的调度任务。

1.1.5> 托盘(缓冲区)

在生产-消费者模式中,形象化描述起来就是托盘;换成操作系统就可以称作临界区,在本人的代码中,被称为缓冲区

这个部分的难点在于需要避免数据竞争,保证线程安全,因为缓冲区涉及到乘客请求的放入和分配器对请求的拿取。所以对于所有在缓冲区内的方法,都需要加锁,防止多个线程交错调用产生各种各样的不安全行为。在这里,由于本人没有使用一些保证了部分存取方法安全的容器类型,而是直接使用了ArrayList,所以对于所有方法都添加了sychronized关键字。

同时,在本人的托盘中存在一个最大容量的属性。当托盘中存储的请求到达了这一最大值,会让当前放入托盘的动作进入等待状态,待请求被取走一些再继续放入。其目的是为了催促消费者即电梯不要堆积任务,尽快完成,理论上听起来有些作用,但在实际测试中对于提升效率的用处并不大。

1.1.6> 输出

保证时间戳递增的输出方法。

一开始本人并没有单独的输出类。但是在第五次强测结果出来并获得了时间戳不递增的结果之后,本人悔改了。时间戳不递增的问题在本地从未出现过,但是多线程同时输出,难免会有不安全的问题,所以确实需要一个为了保证线程安全的输出方法。解决方法很简单,只需要对这个被多个线程调用的输出方法加上sychronized修饰即可。

1.1.7> 类图

 

相对于第一单元的作业,这个架构相对更加清晰一些。

可以看出,代码由主类、读入线程(生产者)、调度器、缓冲区、消费者、安全输出类构成。

Main:负责创建缓冲区,并在此基础上创建读入、调度器、消费者线程。 Request:包装一个请求。后来在互测的时候才意识到,可以直接继承官方包里面的类,使用其中的方法,不用自己再单独写一个。 Allocator:分配器,按楼座为各个消费者分配缓冲区内的请求。 MyBuffer:缓冲区,内部设有put和get方法,用于存放所有请求,同时也可以作为一个用来存放请求的容器被使用。 Producer:生产者,即读入线程,用于读取输入的请求并put进缓冲区;同时在读入结束时发出结束线程的信号。 Consumer:消费者,即电梯,内部设置有电梯的运行方式即各个电梯状态下应该有的行为。 Output:线程安全输出类。

1.2> HW6

1.2.1> 迭代开发

在这次作业中,新增了环形电梯。但是即使有了环形电梯,由于题目限制,我们依旧可以认为每个楼座互相独立,同样的,每层楼也互相独立。环形电梯与纵向电梯之间也互相独立。所以,环形电梯除了可以环形运动以外,和第五次作业并无太大区别。

所以,题目新增的需求要求我们从两个方面对第五次作业的代码进行迭代更新:请求的分配横向电梯的调度

1.2.2> 请求分配

首先,对横向的请求和纵向的请求,这种区分还是很明显的,此处就不加以赘述了。

这里的请求分配重在如何把请求分配给同一层 / 同一楼座中的多座电梯中。指导书提供的思路是按余数进行平均分配。本人一开始采取了随机分配的方法,但交到评测姬中发现时间都非常慢,于是后来换成了向等待队列最短的电梯中放入请求。原因主要在于,这样比较好实现。在第五次作业的时候,本人也畅想过自由竞争的实现方法,但是真正到了需要实现的时候,却并不能形成一个很清晰的思路(但从互测时的观察来看,自由竞争确实快了很多);同样的,还有提前预估各部电梯处理完手头的请求需要的时长,根据时长的长短分配请求的做法,也需要较强的程设能力和清晰的思维,对于本人这种周六才解决完RTLE的蒟蒻来说实在是没有时间思考了。

1.3.3> 横向电梯调度

其实对于横向电梯,和纵向电梯并无太大区别,主要需要解决的问题就是怎么转圈。

本人的公式推导在六作业中是有误的,导致电梯可能会选择更长的一侧(如从C到A选择CDEA的路径)运行,使得性能分不太好看。后来再次破环成链,重新推导了一遍,得到了正确的公式(Q:clockWise;C:counterClockWise):

((st - nd + 5) % 5 > (nd - st + 5) % 5 ? 'Q' : 'C');

而至于处理请求的顺序,和前文提到的纵向电梯别无二致,在此也不再重复了。

1.3.4> 类图

 

Main:负责创建缓冲区,并在此基础上创建读入、调度器、消费者线程。 MyRequest:为了不和官方包撞名字,所以改成了MyRequest,直接继承了官方包提供的PersonRequest,但是增加了一些对于请求运动方向、请求的状态等属性的记录。 Allocator:分配器,按楼座为各个消费者分配缓冲区内的请求。 MyBuffer:缓冲区,内部设有put和get方法,用于存放所有请求,同时也可以作为一个用来存放请求的容器被使用。 Producer:生产者,即读入线程,用于读取输入的请求并put进缓冲区;同时在读入结束时发出结束线程的信号。 Consumer:消费者,即电梯,内部设置有电梯的开关门和达到楼层的行为。 Lift:纵向电梯,设计单独的纵向电梯运行方法。 Belt:环形电梯,名字取材于机场里可见的自动人行道电梯或者说传送带一类的平面传输装置,设计单独的环形电梯运行方法。 Output:线程安全输出类。

 

1.3> HW7

1.3.1> 迭代开发

本次作业中,为了完成一个请求,乘客可能需要换乘了。并且,环形电梯对可到达的楼座进行了设置。

所以,题目新增的需求依旧要求我们从两个方面对第六次作业的代码进行迭代更新:请求的分配横向电梯的调度

但是在分配请求之前,多了一个拆解请求的问题。

1.3.2> 请求拆解

奔着不求性能分只求过的心态,本人直接进行了一个图论的撰写。

一开始想用floyd,后来发现一开始设计的时候维度设计错了又换成了dijkstra,等维度设计对了又懒得换回floyd了。不过数据量较小,所以跑最短路速度还能忍。

本人的做法很暴力也很静态。首先,是建图的环节。由于有5个楼座和10个楼层,于是我们将A座1层映射为1,A座2层映射为2,B座一层映射为11,以此类推,公式即为:(building - 'A') * 10 + floor;。对于可以到达的楼层,则根据该电梯的容量和运行速度属性加权建边(所以是静态的,这里的权重蒟蒻也没能好好设计,所以可能浪费了很多性能)。然后便是跑最短路的过程,是一个非常普通的dijkstra,但是其中每次进行松弛操作的时候都需要记录路径,以便获得最短路径的具体行走方法,用于最终获得请求的拆解结果。

请求拆解的结果是,所有分请求必须按顺序执行,但保证每个分请求的[起点座 == 终点座] + [起点层 == 终点层] == 1,这样对于之前的请求分配和调度算法改动最小(适合比较懒的蒟蒻,不适合喜欢性能分的大佬)。

所以,可以看到以前单个请求的存储方式不再能满足这个设计了。本人改用了请求序列来存放一个读入的请求被拆解的结果,但是每次只处理改队列的队首请求,然后依次处理后续请求,每处理完毕则删除,直至清空整个队列。

1.3.3> 请求分配

这里和上一次作业的区别在于,众多电梯中需要挑出那些个能运送这个请求的电梯,否则就会像本人一样失去强测的分数。

所以,先特判出能送这个乘客的电梯,然后再选择等待队列最短的电梯,放入这个请求。

1.3.4> 电梯调度

由于本人的实现方法,在获得请求的时候就已经把请求拆解成为可以套用第六次作业的调度方法来处理的请求了,所以第七次作业中的电梯调度没有做新的修改。

但是这样的风险非常大,最后得到的性能分也是两极分化严重。

1.3.5> 如何结束

在本次作业中,由于一个请求可能对应一个有多项的请求序列,所以需要更加注意的地方是如何判断当前的所有请求都被处理完了,以结束当前电梯的线程,同时保证线程结束时没有人还在等待换乘或还卡在电梯里没能到达目的地。这里本人选用了上机中提供的代码使用的信号量来解决这样的问题。请求的总数在读入线程中是可以被记录并清楚确定的。那么,每次清空一个请求序列的时候,释放一个“资源”;需要结束线程的时候,判断是否有资源可以被获取,如果有就拿,没有就等着,直到有请求被处理完毕释放“资源”,就可以给拿取了。其中判断次数为请求的总数。

多线程的信号量可以保证,在整个进程结束时,所有的请求都被处理完毕

1.3.6> 架构图

 

这次作业的类又变得更加丰富了起来。

Main:负责创建缓冲区,并在此基础上创建读入、调度器、消费者线程。 Unit:包装一个请求。直接继承了官方包提供的PersonRequest,但是增加了一些对于请求运动方向、请求的状态等属性的记录。 MyRequest:请求序列,存有一个请求经过最短路处理后得到的所有分请求。 Allocator:分配器,按楼座为各个消费者分配缓冲区内的请求。 MyBuffer:缓冲区,内部设有put和get方法,用于存放所有请求,同时也可以作为一个用来存放请求的容器被使用。 Producer:生产者,即读入线程,用于读取输入的请求并put进缓冲区;同时在读入结束时发出结束线程的信号。 Consumer:消费者,即电梯,内部设置有电梯的运行方式即各个电梯状态下应该有的行为。 Consultant:顾问。思考良久都没能想出一个适合的名字,最后对这个拆解请求的程设法最短路径运行者付与了顾问一职,深表敬畏。 Output:线程安全输出类。

1.4> 顺序图

由于三次作业是迭代设计,但是对于线程之间的交互和处理顺序从本质上将没什么区别,故在此指放一张第七次作业对应代码的顺序图,以示本人多线程作业的思路。

首先,在读入一个乘客的请求之后,跑最短路,将请求拆解,放入缓冲区。在读入一个新增电梯的请求之后,新增一个电梯并运行该线程,同时重新建图。

第二,分配器从缓冲区拿取请求,将其放入对应电梯的等待序列。

第三,电梯按照其调度策略处理该请求,若该请求仍需换乘,则重新放回缓冲区,继续处理;否则,完成此任务,释放资源,信号量+1。期间不断山城输出

第四,若读取到线程结束,则设置所有等待队列的状态为结束态,并回收所有资源

 

二 / 捉虫大战

本次作业中,本人并未尝试编写评测姬,一则是作业难度大导致没有时间再去大量测试了,二则是没有研究明白如何使用popen,无法做到通过自己写的评测姬实时投放请求。这也导致了很严重的后果,即本人的代码被强测和互测hack得千疮百孔不忍直视。

最最重要的bug来源依旧是没有仔细阅读指导书

2.1> 自测bug

这个部分用来记录自己出现过的bug。

首先在第五次作业中,本人理解错了到达一层需要输出arrive信息这句话的意思,并且没有详细阅读样例,导致这个蒟蒻设计的电梯只在需要开门出入人的层才输出了arrive信息,以至于丢失所有性能分,并在互测房中获得了被hack20/20的好成绩。其次,由于本地测试并未出现输出线程不安全的情况,于是本人没有意识到封装线程安全类的必要性,在强测中出现时间戳不递增的问题。第三,本人的调度设计非常糟糕,荣获了不少RTLE,在修复的时候也废了不少心思,最后意识到,当时设计的捎带处理逻辑每次选取的只是同方向且能捎带的,是时间最早的而不是距离最近的,于是显而易见,这样的调度很热爱已让电梯陷入反复横跳的境地,浪费时间。在修改完请求处理顺序的逻辑之后,便顺利的修复了所有的bug。

第六次作业中,本人首先利用了第五次作业bug修复的代码预览处,解决了一个由于手抖打错,把每次处理本层到下一个请求的起始楼层的方法写成了处理从本层到下一个请求的目的地楼层,从而(调了一天)导致的RTLE问题,在强测中终于取得了一个可堪入眼的成绩。其次,尝试解决了横向电梯不沿最短方向运行的问题,是解决了一些手头的数据但也没有完全解决所有问题,在前文已经阐述过了。

第七次作业中,依旧出现了RTLE的问题,而且令人绝望的是115s的时间要求本人的代码需要117s跑完。问题依旧出在调度策略上,每次只处理一个请求,致使时间被拉长。

所以说,整个第二单元都是被可恶的RTLE包围的状态,在互测和强测中我因为线程不安全的问题除了第一次的输出时间戳不递增的问题外并未被hack出来,可能主要的问题还是别的错误太多了吧(叹气)。

2.2> 互测bug

第五次作业,由于当时已经意识到自己是一个挂在强测房里的灵魂,于是早早倒下,没有再出刀。

第六次作业中,本人意欲大开杀戒,但是并未造出什么可靠的数据,只是把所有关于横向电梯的数据都喂给了各个程序并发现了有RTLE的情况,最后发现是该战友的电梯以为还有乘客没上来,一直在空转。所以线程的结束是一个很重要的问题。

第七次作业中,本人也是暴力造出了所有可能的数据,并挑选了运行时间最长的50条请求,针对一位同志的超乘、另一位同志的不使用新电梯所以会RTLE的问题、第三位同志的轮询还有同层的横向请求无输出的情况进行了攻击。当然,观察到房内每个人的成功率都在66%,蒟蒻也早该意识到她这次的强测所遭遇的不测。

三 / 度量分析

3.1> 代码长度

同比代码行数多了很多,每次迭代的工程量都不小,但是里面不乏很多重复的可以整合的代码。

3.2> 复杂度分析



可见,三次作业的复杂度都不低,这里只选取了总表和有红色标出的部分表格。

复杂度高的地方主要为电梯调度方法和程设法的图论顾问,蒟蒻目前除了继续提取类似代码以外也确实没有什么解决办法了。同样的,复杂度高的地方出错概率也高,这点在这次的bug中也有很好的印证。

四 / 心得体会

本单元意图教会我什么是多线程,如何处理多线程。

或许,蒟蒻只能说或许,她可能懂得了一点点多线程的入门,懂得了一点点为什么要上锁(解决数据竞争)、怎么上锁(同步块、读写锁等);两个线程的执行顺序是怎样的(随机),线程又是怎么被调度的(可能同期的OS课涉及这类内容更多);线程怎么互相让位(wait/sleep),又怎样唤醒(notifyAll);为什么会出现线程不安全(随机性强,锁上的不准确),怎么产生的死锁(都在等待)、活缩(都在干自己的事情,但无法互相唤醒交互)、轮询(不使用wait/notify)等等不正常的现象。至少她现在以不像刚刚拿到一份training中错误的多线程代码时和该程序期待着它的完结一样绝望了,她已经掌握了一点点调试多线程的方法,无论是随地分发System.out.println()还是(在需要自己挑选的地方)打断点(利用过于方便的IDEA)进行单步调试,都能帮助她加深对于多线程代码是如何运行的理解。

所以,这个单元的设计是成功的,但从所获得的成绩来看,本人是失败的。

但她真的只得到了挫败感吗?并不,在一次次电梯超出逻辑的运行方式中她也在磨炼自己的性子,静下来调试、研究,从奇异的输出中获取乐趣,从捉虫成功中获取动力;在一次次作业的push下努力学习设计模式,从工厂模式、组合模式到生产-消费者模式、观察者模式、单例模式、装饰器模式,这些奇怪的抽象概念一点点变得清晰,虽说在运用中仍有许多困难,理解中仍有许多障碍,但是获取知识的过程远比获得成绩单的打击要快乐许多。

撑过电梯月,俺歪三仍是一条好汉!

posted @ 2022-04-30 22:01  emilyu  阅读(69)  评论(2编辑  收藏  举报