OO第二单元总结

OO第二单元总结

一、作业分析与总结

1.1 第五次作业

1.1.1 题目简述

第五次作业要求我们模拟一个多线程实时电梯系统。

系统基于一个类似北京航空航天大学新主楼的大楼,大楼有 A,B,C,D,E 五个座,每个楼座有对应的一台电梯,可以在楼座内 1-10 层之间运行。系统从标准输入中输入请求信息,程序进行接收和处理,模拟电梯运行,将必要的运行信息通过输出接口进行输出。

本次作业电梯系统具有的功能为:上下行,开关门,以及模拟乘客的进出。电梯限乘人数、速度、可停靠楼层均固定,需要我们编写调度策略来控制电梯运行,同时采用多线程的方式支持五部电梯的同时运行。

1.1.2 基本思路

在本次作业中,主要需要完成两个部分

首先是编写针对一部电梯的调度策略。题目对于电梯的性能做了一定的约束,要求以ALS调度策略为性能基准。经过大量阅读往届学长的博客,以及自己的反复权衡,我最终选择了look策略。

look策略相对于ALS策略,其最大的难点在于电梯转向条件的编写。我们知道,look策略的基本思想是将同方向上所有的请求完成后再转向,但如果仅检测同方向的请求,可能会导致当前方向上仅有反方向的请求,但是电梯却接不到人的情况发生。因此我的思路是,检测未能捎带的请求中是否有起始楼层在当前方向上,同时检测是否有已经捎带的请求还未完成,若两者都不满足则转向,若两个反向上该条件均不满足,则暂停。当转向条件设置成功后,电梯便可以正常运行。

其次是编写多线程的部分。本次作业要求同时考虑五部电梯,因此多线程的部分也至关重要,我们需要着重关注线程安全这个问题,既要保证程序的逻辑在多线程的状态下仍然能够正确执行,也要保证程序运行中不会出现轮询和死锁。

这里我主要使用了生产者消费者模式,通过一个线程来获取所有的请求,一个线程来负责将请求分发给所有的电梯,每个电梯都有自己的未完成请求队列,该队列只和调度器线程共享。基本的框架和上机时的代码一致。

1.1.3 调度器设计

设计思路

在本次作业中我的调度器负责将InputThread线程接收到的请求按楼座分发给各个电梯,其接收的参数有一个请求队列以及一个请求队列数组。请求队列中包含了InputThread线程接收到的请求,请求队列数组中是每一部电梯的未完成队列。

在本次作业中,由于每个楼座只有一部电梯,且不涉及横向电梯与换乘,故调度器分配的逻辑很简单,就是按照楼座将其分入对应的电梯。

线程交互

在与别的线程的交互上,调度器主要和InputThread线程与各个电梯运行线程相交互。其与InputThread线程共享一个请求队列,与电梯线程共享一个未完成请求队列。运行过程中,调度器线程不断从请求队列中获取请求,如果获取到了请求,就根据请求的楼座将请求添加到对应的电梯的未完成请求队列。当调度器线程与输入线程共享的请求队列被输入线程标记为输入结束,且请求队列为空时,将五个电梯线程的未完成请求队列设置为工作结束,并退出循环,结束线程。

1.1.4 架构模式

UML类图

UML协作图

本次作业采用了生产者消费者模式,故基本思路是不断获得请求并将其分配给下一级,每一级之间共享一个请求队列,当输入结束时为所有队列设置结束标志。

1.2 第六次作业

1.2.1 题目简述

第六次作业需要模拟一个多线程环形实时电梯系统。

这次作业相较于上次新增了许多要求:

  1. 每个楼层新增了横向的环形电梯,可以在五个楼座之间循环运行。
  2. 一个楼座或是一个楼层可以有多部电梯
  3. 电梯的速度不同

1.2.2 基本思路

虽然看似题目变复杂了,但其实理清新增的需求之后不难发现,本次作业主要的部分只有两点:编写横向电梯的运行策略、编写多部电梯的请求分配策略。

对于横向电梯的运行策略,我编写了两种:一种是无脑环形运行,另一种是基于可循环运行电梯的look策略。第一种顾名思义,当电梯的未完成请求队列不为空时,就沿着一个方向前行,否则就停下来。第二种策略和纵向电梯的look策略相仿,只是在每一层时根据该层来判断相对的高低。

第一种策略听起来很容易被卡时间,但当我尝试在bug修复时提交该策略,发现与强测的时间差距很小,只有一个点相较于look策略慢了4秒左右。

对于多部电梯的请求分配策略,由于时间关系,我采用的是平均分配,而不是自由竞争。平均分配的实现难度较低,只需在调度器中新增一个循环计数器即可,而且相较于自由竞争,不会出现各种奇怪的轮询或是线程不安全等bug。但是平均分配可能会造成性能的损失,在第六次作业中体现的不明显,但是在第七次作业中我就吃了性能的亏。

1.2.3 调度器设计

设计思路

本次作业中我的调度器共分为两层,第一层为主调度器,和第五次作业一样,将InputThread接收到的请求按楼层和楼座,分配到该处的等待队列中。第二层为子调度器,负责将每个楼座和楼层的等候请求按策略分配给该楼座或楼层的多部电梯。

而子调度器的请求分配策略,我采用了平均分配的方式,将请求均匀的分配给该楼座或楼层的多部电梯,这样做的好处是实现难度较低,且出现bug的概率小,而且由于第六次作业中电梯之间容量和速度的差异不明显,平均分配的策略还是有着不错的性能。

线程交互

在与别的线程的交互上,主调度器线程主要和InputThread线程与子调度器线程相交互。其与``InputThread线程共享一个请求队列,与子调度器共享一个分配队列。运行过程中,主调度器线程不断从InputThread`线程中获取请求,如果获取到了请求,则将其分配到不同的楼座或楼层。当输入线程与输入线程共享的请求队列被输入线程标记为输入结束,且请求队列为空时,将分配队列设置为工作结束,并退出循环,结束线程。

而子调度器线程与电梯线程共享一个未完成请求队列。运行过程中,子调度器线程不断从主调度器中获取请求,如果获取到了请求,则对多部电梯进行平均分配。当主调度器线程与共享的请求队列被输入线程标记为输入结束,且请求队列为空时,将电梯线程的未完成请求队列设置为工作结束,并退出循环,结束线程。

1.2.4 架构模式

UML类图

UML协作图

本次作业采用了生产者消费者模式,故基本思路是不断获得请求并将其分配给下一级,每一级之间共享一个请求队列,当输入结束时为所有队列设置结束标志。

1.3 第七次作业

1.3.1 题目简述

第七次作业需要模拟一个多线程环形实时电梯系统。

这次作业相较于上次新增了许多要求:

  1. 增加了需换乘的请求,即一个请求的楼层和楼座均不相同
  2. 增加了电梯的定制化功能,可对纵向电梯的运行速度、可容纳人数,横向电梯的运行速度、可容纳人数、可开关门信息进行定制

1.3.2 基本思路

本次作业的难点主要在于换乘请求的判断与拆分,以及增加了换乘请求后流水线模式的架构编写。

课程组为我们提供了两种思路,分别是:

  • 动态拆分:电梯分析整体的请求,尽量将请求往目的地捎带
  • 静态拆分:跟据路径可达信息在请求读入时拆成多个阶段任务,电梯按阶段接受任务

考虑到时间与架构,我选择了静态拆分的方法,在接收到请求之后便对其进行拆分,根据当前已有的电梯信息,寻找最短的换乘路径,并将需要换乘的请求拆分成两条或三条无需换乘的请求,并将其封装成一个数组,对外只暴露其第一条请求,然后将其当做普通的请求进行分配,如果完成后发现还有后续的请求,便将其还给主调度器,并由主调度器完成请求的再次分配。

对于当前的多阶段请求,原来的生产者消费者模式便不再适用,需要引入流水线模式。在本次作业中,我们需要在读入请求时对请求进行计数,并在每一个请求彻底完成之后进行请求的验收,之后才能够对每一个线程标记结束。

1.3.3 调度器设计

设计思路

本次作业中我将请求的拆分在InputThread线程进行,而调度器仍分为两层,基本结构与第六次作业相同。不同的是InputThread线程增加了对请求的计数和验收,之后再对主调度器设置结束标志。同时主调度器要完成可换乘请求的再次分配。

线程交互

在与别的线程的交互上,InputThread线程负责请求的接收与拆分,将拆分后的请求与不需拆分的请求放入总请求队列,并与主调度器线程共享请求队列。同时,InputThread线程需要对输入的请求进行计数和验收,所有请求均验收完成之后再对主调度器设置结束标志。

主调度器线程在本次作业中变得很复杂,其需要和InputThread线程与子调度器线程相交互,同时需要将自己的待分配队列与各个电梯共享。

  • 首先,其与InputThread线程共享一个请求队列,与子调度器共享一个分配队列。运行过程中,主调度器线程不断从InputThread线程中获取请求,如果获取到了请求,则将其分配到不同的楼座或楼层。

  • 其次,其与各个电梯共享它的待分配队列,若电梯完成了可换乘请求的一部分,则需要将剩余的请求重新还给主调度器。

  • 当输入线程与输入线程共享的请求队列被输入线程标记为输入结束,且请求队列为空时,将分配队列设置为工作结束,并退出循环,结束线程。

而子调度器线程与电梯线程共享一个未完成请求队列。运行过程中,子调度器线程不断从主调度器中获取请求,如果获取到了请求,则对多部电梯进行平均分配。当主调度器线程与共享的请求队列被输入线程标记为输入结束,且请求队列为空时,将电梯线程的未完成请求队列设置为工作结束,并退出循环,结束线程。

1.3.4 架构模式

UML类图

UML协作图

本次作业在生产者消费者模式的基础上,针对换乘请求增加了流水线模式,故基本思路是不断获得请求并将其分配给下一级,每一级之间共享一个请求队列,同时,电梯和主调度器之间共享队列,当换乘请求的一部分完成后,将其还给主调度器,重新进行分配。当输入结束并且所有任务均验收完毕时,为所有队列设置结束标志。

二、同步块的设置和锁的选择

2.1 同步块设置

在这三次作业中,由于共享的部分大多数为请求队列及其方法,因此将其抽象为RequestQueue类,并将其中涉及到读写的方法设置为同步块,主要有:

public synchronized void addRequest(PersonRequest request) {
    requests.add(request);
    this.notifyAll();
}
public synchronized PersonRequest getOneRequest() {
    if (requests.isEmpty() && !isEnd) {
        try {
        	this.wait();
        } catch (InterruptedException e) {
        	e.printStackTrace();
        }
    }
    if (requests.isEmpty()) {
    	return null;
    }
    PersonRequest request = requests.get(0);
    requests.remove(0);
    notifyAll();
    return request;
}
public synchronized void setEnd(boolean isEnd) {
    this.isEnd = isEnd;
    notifyAll();
}

public synchronized boolean isEnd() {
    notifyAll();
    return isEnd;
}

public synchronized boolean isEmpty() {
    notifyAll();
    return requests.isEmpty();
}

还有就是在对电梯在遍历未完成请求队列时,需要对该队列进行同步,其操作大致如下:

synchronized (reqQuene) {
    for (int i = 0; i < reqQuene.size(); i++) {
    	int from = reqQuene.getRequests().get(i).getFromFloor();
        if (from < elevator.getFloor()) {
        	firstTurnTo = false;
        }
    }
}

2.2 锁的选择

在这一单元的作业中,我的锁的设计均采用synchronized的方法,较为方便。缺点是不够直观,无法像ReentrantLock那样直观,而且这样设置会使得程序中的锁太重,本来只需要读不需要写的场合也会因为上锁而陷入等待,从而严重影响性能。

三、程序bug分析

3.1 第五次作业

第五次作业的bug主要有两处。

  • 第一处是输出线程不安全导致的时间戳非递增的问题,解决方法是新建一个输出线程安全的类,对TimableOutput.println(s)方法加锁使其同步。
  • 第二处bug比较难找,其与训练代码中task3有些类似,其问题出在我对于电梯状态转移条件设置不完全,使得当电梯线程处在stop状态且陷入sleep时,最后一条请求读入并notifyAll无法起到作用,当sleep结束时电梯从stop状态开始wait,然而由于是最后一条请求,之后就没有notifyAll了,故不会从stop状态改变,从而无法完成最后一条请求且无法结束线程运行。解决方法是完善电梯状态转移条件,使其在判断电梯wait时不至于出现上述情况。

3.2 第六次作业

第六次作业由于严格按照第五次的电梯运行策略与平均分配的调度策略,故没有出现bug。

3.3 第七次作业

第七次作业同样按照上述策略,故没有在逻辑上出现bug。但是由于我过于循规蹈矩,不愿在前两次作业的基础上做出改变,因此性能分较低。

3.4 hack策略

在本单元的作业中,我的hack策略主要是手动构造数据测试。

第五次作业中主要是构造能引发输出线程不安全,时间戳非递增的bug的数据,同时构造一些频繁转向,楼层跨度较大的边界数据来进行测试。第六次作业中hack策略与第五次相仿,同时还可以针对多部电梯同时运行构造能够引发线程不安全问题的数据。第七次作业中主要是针对电梯换乘来构造数据,构造大量需要换乘的请求,或是构造一些不得不在1楼换乘的请求来进行hack。

四、心得与体会

4.1 多线程

本单元的作业着重于训练我们多线程编程的能力。首先,我们需要理解并发程序的原理,然后了解多线程程序相较于单线程程序可能出现的线程不安全、轮询、死锁等问题的原因,然后学习如何通过同步块的设置来避免这些问题。通过三次电梯作业的实践与巩固,我确实更加深刻的理解了多线程编程,收获良多。

可惜在这一单元的学习中,由于OO和OS的压力过重,我没有时间去实践多线程编程中的ReentrantLock锁以及读写锁等更加灵活的同步块设置的手段,在作业完成过程中,也有一些想法没能实现。在今后的学习中,我希望能够继续探索与拓展。

4.2 层次化设计

同时,从三次电梯的迭代与升级中,我认识到了顶层设计和架构设计对于一段程序、一个项目的重要性。一个层次逻辑清晰,扩展性强的架构,对于后续的维护与开发有着重大的作用。

以本单元为例,我明显感觉到在每一次的作业开始前都无法很好的利用上一次作业的基础,总要做出重大的修改才能满足新增的需求,而且在作业完成时,我也能够感觉到当前架构的局限性,这就说明我在完成作业时并没有为后续的扩展留出空间。今后,我也应当更加注重层次与架构的设计,在一个项目开始时就要做好充分的设计,才能够保持后续高效的开发。我应一直秉承这一思想,并将之应用到之后的作业与实践当中。

posted @ 2022-04-29 21:46  Levelower  阅读(27)  评论(0编辑  收藏  举报