BUAAOO 第二单元总结
BUAAOO 第二单元总结
前言
什么是真正的好电梯?每次在公寓楼被满载开门打打招呼,又撂下悲伤、可怜、弱小、无助还要迟到了的我扬长而去的电梯,气到早八血压飙升冲顶的我,在质疑为什么电梯各种意义上这么渣的同时,也难以从脑中排挤出这个难以有结果的问题。
对乘客来说,答案是显而易见的。在每次需要时及时赶到,在每次乘上后直奔目的地不停歇的电梯,显然是好电梯。
然而,一旦从电梯的角度考虑,却将当即发现自己身陷没有结果的修罗场之中。于未知的时段、以未知的人数、带着未知的请求,扑向自己怀抱的乘客的爱太过沉重,不可避免地将大脑淹没,停止思考成为憨憨。
爱是理解的别名。
为了从电梯的鱼塘里爬出,彻底摆脱被电梯PUA的命运,重拾与电梯健全的关系,在三周内,我——成为了电梯。
同步块的设置和锁的选择
抽身修罗场。
成为电梯的第一步,是分析电梯所面临的窘境。
乘客,被抽象成由出发点与目的地完全定义的请求。接受请求、消解请求,是这台在电脑上上上下下、永无停息的电梯单元所处理的问题的两个方面。
乘客就是月饼!这番自我矮化般的类比缘起于我所在班级的助教在解说生产、消费者模式与多线程编程时,援引学校窗口买卖月饼的实例做出的相当具象的解释。既然如此,正如助教学长的那篇博文所述,解决问题的最直接方法,便是设计高级的桌子。
为了便于问题的解决,我需要 桌子 具备这样的特点:
- 低耦合。模块化、粒度小而具灵活性;
- 读写分离。生产者——输入线程,和消费者——电梯线程不会争抢同一位乘客,以防止将其扯成两半幽灵(线程不安全出现冗余请求);
- 有序性。乘客友好型的电梯尽量依序提取请求。
当然,最后一点理应打上问号。在本单元中,评价电梯的标准,除一般的请求等待时间外,还包括电梯的总运行时间,如果提高前者在调度考虑中的权重,可能带来额外的运行时间。
从以上三点考虑,具体到电梯情境,我需要这样的 大楼 :
- 外部接口单一,内部楼层分离——提供统一接口的容器组;
- 队列一头进电梯,一头乘客排队——读锁写锁分离,以提高吞吐量;
- 先来先到——FIFO队列,以实现同楼层请求的相对公平,减少极端等待时间。
这时,我意识到java提供的一种数据结构完美地一举满足了以上后两个维度的需求——LinkedBlockingQueue。
通过直接调用其基于分离读写锁实现的,可并行的 offer
与 poll
操作。这一并行的实现,可以采用以下这张资料图进行解说:
读写锁分别控制头部和尾部,从而实现了读写分离并行状态下的线程安全。
有了如此高级的桌子,我们只需保证只通过上述两个接口实现原子性的读写操作,便无需担心线程安全问题。
最后,为了满足上述第一点请求,我们只需将指定数量(在这里是20层)的楼层列表封装成一整幢建筑,基于并行场景下的数据容器设计便完成了。
调度器设计
将大脑交给上帝。
既然电梯在我的设计中,变成了只管开门关门上上下下拉人放人的憨憨。那么调度器的设计,即是在设计电梯单元的核心大脑。
在考量调度器的设计之前,应当分析如下几个方面:
- 输入输出条件。调度器所能接受的信息,是乘客的状态与电梯的状态的综合。具体说来,前者包括各楼层乘客的数量及其需求;后者则包括电梯的位置、内部乘客数量及目的地。那么调度器需要得出什么分析结果呢?——对于我的电梯来说,在拉人的方面是贪婪的:只要能带上,就全带上,从而利用贪心的思路尽量减少乘客的等待时间。既然这个策略无需另行决定,调度器所要给出的指引便只余一点——电梯需要去哪里。
- 输入条件的特点。无论是实践经验还是本次作业的情境中,乘客请求永远具有不可预测性(除特例night外);同时,虽然没有线程安全问题,但我的电梯在处理请求时,仍然具有竞争的特性,线程的竞争同样难以预测。因此调度器同样可以采用贪心的思路,根据当下情境输出满足给定策略的最优解。
因此,对于单个电梯,我的调度器优先选择最近的请求指派电梯满足。
对于多个电梯,我的调度器采用合理的idle策略,确保空电梯在不同层待命。
对于多种电梯,我的调度器根据类型予以指定。
此处还需强调我对于换乘的处理——既然调度器已经过于复杂,那么就将部分逻辑交给乘客——构建乘客的同时,乘客将根据自己的目的地和出发点与电梯种类的匹配性,决择自己的换乘方式,基本思路是:选择尽量快的电梯(后两类型的电梯)。各自干好该干的事,最后的结果肯定不差。这就是我在三次作业中行之有效的调度思路。
第三次作业架构设计
模块化设计拯救屎山。
虽然要求分析第三次作业的架构,但本单元作业的架构设计从一开始便进行统筹布局,因此几乎没有变化。
项目类结构说明
生产者——输入线程、消费者——电梯线程是最核心的部分,“桌子”——Building
容器组合请求列表。此外,调度器作为工具类进行抽象,接收建筑、电梯单元——电梯的形式组合容器两个实例,根据其信息进行调度输出。值得一提的是,调度器只输出电梯目的地,因此电梯单元内部“座舱”数据结构独立于外界操作,避免了调度器对“座舱”访问与操作带来的潜在线程安全问题。
本架构在以下几个方面进行了扩展性考虑:
- 电梯类:关键参数不写死而在构造时确定,而具体的操作逻辑则统一,前一考虑在第三次作业意料之中地体现了其优越性,后一考虑则定义了电梯的共通特征,使得调度器不用为每种电梯进行从头开始的策略设计;
- 乘客类:我的设计对乘客请求同样进行了包装,定义了换乘方法。具体而言,我的设计中的乘客,具有最终目的地、中转目的地、中转标志三个冗余特征,在换乘逻辑中,电梯对乘客的“放出”操作由乘客的“中转标志”确定,如果已到目的地,则将其移出请求列表;否则,调用其
megaEvolve()
方法,乘客将根据自己的最终目的地、中转目的地,重定义呈现给电梯的出发点和目的地,生成新的乘客,并由电梯将新的乘客请求放入对应请求列表中。同时,乘客请求也有与电梯类型对应的乘客类型,降低了验证逻辑及下文“滤除策略”的实现难度; - 调度器:为实现多类型电梯的兼容,又复用上一部分提到的基本逻辑:“去向离自己最近的可搭乘请求”,调度器在完成该逻辑前,调用了根据电梯类型过滤请求列表——将不属于该类型的请求滤除的内部方法,并在其中定义并根据电梯类型采取对应的滤除策略。
回过头来看,虽然这一架构足够层次化和模块化,有效地帮助我完成了三次作业的编写,但仍然存在相当显然的可改进之处:如果实现了不同种类请求列表的分离,将进一步解耦请求类型解析逻辑,降低实现难度。
线程协作关系图示及说明
主线程完成了本项目结构各构件的实例化。其中,建筑容器和电梯单元容器进一步完成请求列表和电梯的初始化。完成初始化后,输入线程及各电梯线程开始运行,并行地与各楼层的请求列表,在严格的访问控制策略下读写交互。
Bug分析及Hack思路
三次作业均没有在公测和互测中出现Bug,但具体到每次作业的调试阶段各有体会。总的来说,本单元聚焦多线程编程,与第一单元穷尽心思设计特例数据不同,在进行调试和 Hack 时对线程安全问题格外关注。
第一次作业
阻塞链表队列数据结构的使用,有效地避免了可能产生的并发状况下多线程Bug。然而,在自己调试的阶段,却出现了一个始料不及的问题——从自动机视角涉及的,基于状态转移的电梯,由于状态转移路径的设计失误,导致了“到达”信息的重复输出。这启示我状态机设计的重点不仅在于状态的合理分解和设定,更再于转移路径的单一化、明确化。
在 Hack 的部分,采用了边界条件、极端条件数据设计和尝试的 Hack 思路,然而,由于未设计适用于本场景的评测机,导致输入输出验证全靠怒目圆睁对屏幕,最终无果而归。解匿之后,却发现确实有一位同学的代码便折戟于此。
第二次作业
由于对于队列的并发控制毫无问题,本次作业得以又好又快地一次完成,Free of Bugs! 这启示我并发多线程程序设计的重中之重——访问控制的处理。
互测同样基于压力测试的思路,竭力构造存在竞争可能的样例。然而多线程的竞争存在不确定性,线上被他人测出的同种Bug,在本地却未能复现。
第三次作业
由于前次作业的可扩展性优秀,本次作业也无Bug,并经受住了同学互测阶段的密集轰炸。
来到第三次作业,Hack 时主要聚焦于对代码的静态分析,考量电梯类型处理、换乘策略的正确性。
心得体会
渣男电梯终变憨憨中央空调。在这一整个蜕变的过程中,最令我印象深刻的是——一个好的架构有多么重要。对于访问控制的设计(见上文第一部分)、对于整体结构的模块化设计和组合(见第三次作业分析),使我的电梯体验顺利到几乎无需赘言。
OO大冒险,堂堂连载,且待下周继续勇者斗恶龙!