信号量(生产者和消费者模型)

信号量和管程都是操作系统用于同步提供的两种方法,我们将结合生产者与消费者模型对此进行学习。


什么是信号量?

为了提高系统的并发性,我们引入了多进程,多线程,但是这样子带来了资源竞争,也就是多个程序同时访问一个共享资源而引发的一系列问题,因此我们需要协调多线程对与共享资源的访问,在任意时刻保证只能有一个线程执行临界区代码。为了实现同步,我们可以利用底层硬件支持,也可以利用高层次的编程抽象,信号量和管程属于后者,是高层次的一种抽象,如下图:

信号量(生产者和消费者模型)_第1张图片

信号量的定义:

信号量(semaphore)是一种抽象数据类型,它是由一个(sem)整型变量(用于表示资源数目)和两个原子操作P,V组成。
这两个操作分别是:

P操作:在申请资源时候使用,将sem减1,如果sem小于0,就进入等待,否则直接使用资源即可,注意P操作有可能因为没有资源进入阻塞

V操作:在释放资源时候使用,将sem加1,如果sem依旧小于等于0,说明之前有进程在等待使用这个资源,因此需要唤醒一个等待进程

信号量的实现:

由定义,我们给出信号量的伪代码实现:

class Semaphore
{
    //sem只能由P,V进行原子操作
    int sem;
    WaitQueue q;
    P()
    {
        sem--;
        if (sem < 0) //说明没有资源
        {
            //将线程t放入等待队列q中
            //阻塞
        }
    }
    Q()
    {
        sem++;
        if (sem <= 0)//说明有其他线程在等待使用该资源
        {
            //从等待队列中移除线程
            //唤醒
        }
    }
}

信号量的分类和使用:

信号量可分为两种:

二进制信号量:资源数目0或者1
资源信号量:资源数目为任意非负值
这两者等价,基于一个可以实现另一个

信号量一般用于两种情况:

互斥访问:临界区的互斥访问控制

我们看一个例子,假设有一个共享资源,进程P0,P1对它进行操作,我们希望P0,P1都是互斥访问这个共享资源,那么我们该如何用信号量实现临界区的互斥访问?

我们为此资源设置一个信号量,初值为1,mutx = New Semaphore(1)

mutex->P();
Critical Section;//临界区操作
mutex->V();

由于P0,P1访问顺序的不确定性,我们不妨让P0先访问(P1同理),P0执行mutex->P(),将资源mutex数目由1减为0(这一步是原子操作不可打断)

此时,如果P1不行进行访问,那么P0会顺利执行临界区操作,如果P1进行访问,那么由于P0已经执行了对应的P操作让sem=0,P1自己在执行P操作过程中,sem减1等于-1,那么P1进程会阻塞自己,进入等待队列。

直到P0访问完临界区,执行V操作,将mutex加1等于0,去唤醒P1进程,P1才会去访问共享资源。

通过这样的一种机制,完成了对于临界区的互斥访问。

条件同步:线程之间的时间等待

同样的,我们举例说明,有两个进程A,B,他们会执行各自的指令

信号量(生产者和消费者模型)_第2张图片

其中,线程A必须等线程B中X指令执行完才可以执行N指令,比如说X是接受数据,N是处理数据这样的操作等。。。

为了实现这样的同步机制,我们条件设置一个信号量,其初值为0

condition = New Semaphore(0)

信号量(生产者和消费者模型)_第3张图片

由于A,B的执行次序不定,我们分类讨论。

  1. B先执行:B执行到V操作时候,将condition从0加为1,此时如果切换到A执行,那么A执行到P操作,发现condition-1为0,可以继续执行N指令
  2. A先执行:A执行到P操作时候,将condition从0减为-1,由于condition<0,则A会阻塞自己,接下来B执行到V操作,将condition从-1加到0,此时B知道有进程在等待资源,因此它去唤醒A,A因此执行了N指令

通过这样的机制完成同步等待。

生产者和消费者模型

信号量(生产者和消费者模型)_第4张图片

信号量(生产者和消费者模型)_第5张图片

在上述图片中,将mutex信号量用于完成互斥访问,full,empty则用于完成问题分析中两个条件同步,因此我们将一个实际问题转化为信号量可以解决的问题

信号量(生产者和消费者模型)_第6张图片

如上图,我们在类中定义了三个信号量用于完成互斥,同步操作,初始时候,mutex表示资源为1,full表示当前满缓冲区的个数为0,empty表示当前buffer都为空,(我们设定缓冲取的大小为n,因此full+empty == n)

首先必须保证互斥操作,也就是任意时刻只有一个线程能操作缓冲区,要么是生产者,要么是消费者,因此我们在Deposit(生产者),Remove(消费者)中分别进行了PV操作。(具体过程和上面互斥访问一致,不清楚可以往前翻看)

下面我们要来处理缓冲区满或者空时,条件同步是如何实现的?

对于生产者,如果缓冲区满,则必须阻塞自己,只有等消费者消费了,缓冲区还有空缓冲区才能够继续生产,因此,我们需要同时改写Deposit(c)和Remove(c)的代码:

Deposit(c)
{
    empty->P();//检查是否还有空缓冲区

    mutex->P();
    Add c;
    mutex->V();
}

Remove(c)
{
    mutex->P();
    Remove c;
    mutex->V();

    empty->V();//消费者读取一个数据,释放一个空缓冲区资源
}

同理对于消费者,如果缓冲区为空,则必须阻塞自己,只有等生产者生产了,缓冲区还有东西才能够继续消费,因此,我们也需要同时改写Deposit(c)和Remove(c)的代码:

Deposit(c)
{
    empty->P();//检查是否还有空缓冲区

    mutex->P();
    Add c;
    mutex->V();

    full->V();//生产者生成了数据,需要将满缓冲区个数加1
}

Remove(c)
{
    full->P();//检查缓冲区里面是否还有东西,若缓冲区为空,则阻塞自己

    mutex->P();
    Remove c;
    mutex->V();

    empty->V();//消费者读取一个数据,释放一个空缓冲区资源
}

信号量(生产者和消费者模型)_第7张图片

你可能感兴趣的:(Linux/操作系统)