BUAA OO 第二单元总结

BUAA OO 第二单元总结

〇.综述

在第二单元的三次迭代作业完成后,笔者认为本单元考察的重点主要有两个:1.以实际问题“电梯调度”为背景初识多线程问题,并对多线程中的线程安全问题着重考察;2.从实际场景中获取灵感调整调度策略,进一步体会“面向对象”思想在问题空间向解空间映射过程中发挥的巨大作用。

巧合的是,在OS课的学习中,同样接触到了进程、线程的问题,而且不断优化调度策略的过程又和OS中页面置换等策略的调整过程感觉很是相似。

一.线程分析

事实上,如果不考虑调度器设计,本单元的线程问题非常清晰明了。从本质上讲,只存在一对冲突线程:向队列中添加需求的InputThread线程与从队列中拿走需求的Elevator线程。

为了最大程度上保证线程安全、保持线程结构的清晰,笔者在实现过程中基于“生产者-消费者”模型,摒弃了调度器的使用,并对纵向电梯与环向电梯进行了划分。即笔者的架构中仅存在以下三类线程:

  • 输入线程InputThread
  • 纵向电梯线程Elevator
  • 环向电梯线程ConveyorBelt

并建立了两个”托盘“类:

  • Building
  • Floor

此时,线程冲突划分得更加清晰:

  • 给Building放置需求的InputThread线程与从Building取走需求的各个Elevator线程(共享对象为Building)
  • 给Floor放置需求的InputThread线程与从Floor取走需求的各个ConveyorBelt线程(共享对象为Floor)

二.同步块的设置

基于上述线程分析,笔者的同步块设置主要用在两个”托盘“类中各方法的保护。

具体地,对于”托盘“类放置需求putReq()方法、取走需求takeReq()方法、各get、set方法等使用synchronized同步块进行保护。

此外,在笔者的架构中还涉及到了两个单例模式的类:SafeOutputSharedPool,前者用于解决输出线程安全问题,后者给各线程提供统一的信息(主要为输入是否结束尚未到达目的地的人数)。对于这两个类的方法同样使用synchronized同步块进行保护,保证线程安全。

由于笔者的架构的共享对象非常有限,因此仅仅使用简单的synchronized同步块即可达到线程安全要求,没有再额外使用锁加以保护。

显然,synchronized同步块进行保护的内容是至少两个线程的共享对象,且同时涉及读写操作。加以保护后保证每次仅有一个线程对共享对象进行访问或修改,保证了线程安全。

三.调度器设计

笔者认为,在本单元的作业中,调度器的作用主要有两个:

  • 人员需求分类,派发给对应的“托盘”
  • 调度电梯线程,控制它们的运行方向

然而实际上,这两个功能的实现较为简单。而前文也已经提到,加入调度器后会使现有的较为清晰的线程问题变得稍显混乱。因此,笔者没有额外地建立调度器类。

而对于上述的功能,笔者将前者放在InputThread类中进行人员需求的分类与派发,将后者的调度策略直接置于电梯类的内部实现。

进一步分析,对于前者,如此实现的一大弊端在于需求在输入的一瞬间就已经派发好,本质属于静态调度。而这种调度方式对于第三次作业而言,在性能上很不具优势;对于后者,在电梯内部实现调度策略,由每个电梯线程自己决定自己的运行方向,使各电梯线程的运行不具有联系,实质上天然地属于所谓“自由竞争”的策略。综合考量,这种实现方式在线程安全方面的问题较少,性能尚可。

四.架构模式

笔者基于“生产者-消费者”模型进行架构。主要有以下关键类:

  • 管理纵向移动的等待队列和横向移动的等待队列的两个类:BuildingFloor。两者本质都是生产者-消费者模型中的Tray,属于线程间的共享对象,需要对各方法进行synchronized保护。

    具体实现方面,两者都采用了以ArrayList<Person>为元素二维数组。即:

    private ArrayList<Person>[][] req;
    

    Building为例,二维数组的第一个下标代表楼层,第二个下标代表方向,数组元素ArrayList即为某层前往某方向的等待队列;

    e.g. req[6][0]代表目前处于六层、想要下楼的等待队列;req[9][1]代表目前处于九层、想要上楼的等待队列

    在类中实现了putReq(), takeReq()等方法

  • 纵向移动的电梯与横向移动的电梯:ElevatorConveyorBelt。两者本质都是生产者-消费者模型中的Consumer,是两类线程。它们有三个状态:

    • 状态0:电梯内有人,向下/顺时针运行
    • 状态1:电梯内有人,向上/逆时针运行
    • 状态2:电梯内无人

    捎带策略与调度策略在后文有详细阐述

  • 输入线程类:InputThread。本质是生产者-消费者模型中的Producer

迭代过程如下:

  • 第一次作业没有环向电梯,因此关键类中只有上述的BuildingElevatorInputThread
  • 第二次作业加入环形电梯,加入FloorConveyorBelt,基本具有上述整体架构;
  • 第三次作业对于Person类做了较多改动,主要是在得到出发地与目的地后立刻根据现有电梯情况规划完整路线,本质属于静态调度。此外,还在ElevatorConveyorBelt类中加入putReq()的方法调用,对应尚未走完规划路线的情况。

图中BuildingFloor仅为关键的共享对象,不代表线程。

其中特别强调第三次作业中各电梯线程的终止条件:输入结束且所有人都到达目的地。当满足终止条件时,所有电梯线程同时结束。对于事件”所有人都到达目的地“是借助使用单例模式的SharedPool类实现的。

五.调度策略

关于调度策略,笔者在开篇的综述中提到”和OS中页面置换等策略的调整过程感觉很是相似“。因此笔者在调度策略的设计中试图采用OS中页面置换策略的类似思路。OS的页面置换策略有FIFO、LRU等等一系列算法,对应到电梯中似乎也可以找到类似的映射关系。

然而,笔者很快放弃了这样的想法,因为两者的统计规律不同。不难想到,OS中各页面置换算法,基于的是”局部性原理“的统计特征。然而,无论在现实生活中还是题目设置中,都不会存在类似”局部性原理“的假设——相同的楼层更有可能再有新的需求。

现实生活中,统计规律可能是从一楼出发的需求更多;而对于题目,由于并不知道数据的统计特征(最有可能是随机分布),因此无法对应地构造出相应的最优策略。

当然,一些基本的优化方案还是可以想到并实现的。下面是笔者在三次作业中采用的主要调度策略:

  • 没有显式的设置主请求与捎带请求。由于两个本质为Tray的类中对等待队列的方向作出了划分,电梯只会让与运行方向相同的队列中的人登上电梯。

  • 当电梯有人时,状态处于0 or 1,每改变一层决定是否开关门,从而实现捎带。

  • 当电梯内无人时,需要重新确定电梯的运行方向。此时以该层以上的平均每层的人数与该层以下的平均每层的人数作为重新确定电梯运行方向的衡量指标,即:

    dir = ((downNum * 1.0 / (curFloor - 1)) > (upNum * 1.0 / (FLOOR - curFloor))) ? 0 : 1;
    

    环形电梯类似,比较顺时针半圈与逆时针半圈的人数

  • 自由竞争 大道至简

  • 加入电梯做好初始化后直接start()线程

笔者个人认为自己调度策略的亮点主要在于第三点。从本质上分析,本架构采用的调度策略与ALS相比,主要优化在电梯中无人后的电梯方向选择。本架构没有采用到达早晚时间的指标,而是采用该层以上以下的平均每层人数作为衡量指标,更加合理,利于提高电梯单位时间的吞吐量

但同时,由于缺少调度器的”天生缺陷“,笔者并没有实现动态调度,因此在第三次作业中没有取得较为突出的性能表现。

六.Bug分析

笔者的三次作业中出现了一个较大的bug且在互测中被hack:第一次作业中没有考虑从到输出线程的安全问题,没有使用单例模式+synchronized保护。产生bug的主要原因还是在于没有对指导书中的内容仔细理解分析。

此外,在coding的过程中,还出现了诸多致命的小bug。特别是在第三次作业,如直接将各电梯的开关门信息M码”或“起来、容器元素边遍历边删除等。由于笔者的架构各线程较为清晰明了,反倒十分万幸地没有遇到线程安全方面的各种”玄学“bug。

而在hack他人的过程中,首先简单阅读了同房间其它同学的代码。由于大家的架构都十分清晰并对线程安全问题做了充分的保护与测试,因此从中很难找到问题。

之后构造样例进行测试,有意加大了边界数据的测试比重(如69.5s投放若干请求等),同时也辅以一些随机生成的数据。然而最终依然没有hack成功。

不过感觉自己在阅读代码的过程中依然收获很大~

七.心得体会

本单元的作业对于笔者个人而言,最大的收获反倒不是在线程安全方面,而是对面向对象设计的思想有了更加深刻的认识。

一方面,实现电梯问题需要引入多线程,正是因为在现实世界中,电梯的接送与人员的进入本身就是一个多线程的问题,因此需要用相应的线程对象对现实过程进行模拟与映射。

另一方面,在基础模型层面上建立起“问题空间”与“解空间”的映射后,相应的调度策略方法实质上也可以根据实际问题中的调度策略进行模拟映射。在笔者三次作业的具体实现上,没有直接将题目中给定的ALS策略进行直接地“翻译”,而是首先建立起一个映射到现实空间的数据结构——一个以ArrayList为元素的二维数组形式的需求队列,然后基于此将现实生活中的电梯调度策略进行映射。做完这些后,由于是直接从现实世界进行映射,因此自己始终对代码的逻辑流程把握得非常清晰;此外,笔者发现自己映射的调度策略本质上和ALS其实极为相似,不过在电梯无人的情况下作了细致的优化。

因此,笔者在本单元最大的心得体会在于:面向对象的设计思想,可以帮助我们更好地完成从现实空间到解空间的映射,而这个映射可以是从基础模型到数据结构再到策略方法的方方面面的映射。

此外,笔者还想到了一种更好地还原现实情况的题目改进方案,即:可以在作业中引入一个表征“耗电量”的正确性上限或性能指标,电梯的移动、开关门等都会耗电。这样一来,不仅可以对相对“无脑”的“自由竞争”策略做出约束限制,还更加真实地反映了现实世界的实际情况,更有利于凸显面向对象设计与构造的优越性。

期待自己能在后续单元的学习中继续深化对面向对象思想的理解~

posted @ 2022-05-03 11:04  Lingo30  阅读(51)  评论(1编辑  收藏  举报