信号量和管程都是操作系统用于同步提供的两种方法,我们将结合生产者与消费者模型对此进行学习。
什么是信号量?
为了提高系统的并发性,我们引入了多进程,多线程,但是这样子带来了资源竞争,也就是多个程序同时访问一个共享资源而引发的一系列问题,因此我们需要协调多线程对与共享资源的访问,在任意时刻保证只能有一个线程执行临界区代码。为了实现同步,我们可以利用底层硬件支持,也可以利用高层次的编程抽象,信号量和管程属于后者,是高层次的一种抽象,如下图:
信号量的定义:
信号量(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,他们会执行各自的指令
其中,线程A必须等线程B中X指令执行完才可以执行N指令,比如说X是接受数据,N是处理数据这样的操作等。。。
为了实现这样的同步机制,我们条件设置一个信号量,其初值为0
condition = New Semaphore(0)
由于A,B的执行次序不定,我们分类讨论。
- B先执行:B执行到V操作时候,将condition从0加为1,此时如果切换到A执行,那么A执行到P操作,发现condition-1为0,可以继续执行N指令
- A先执行:A执行到P操作时候,将condition从0减为-1,由于condition<0,则A会阻塞自己,接下来B执行到V操作,将condition从-1加到0,此时B知道有进程在等待资源,因此它去唤醒A,A因此执行了N指令
通过这样的机制完成同步等待。
生产者和消费者模型
在上述图片中,将mutex信号量用于完成互斥访问,full,empty则用于完成问题分析中两个条件同步,因此我们将一个实际问题转化为信号量可以解决的问题。
如上图,我们在类中定义了三个信号量用于完成互斥,同步操作,初始时候,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();//消费者读取一个数据,释放一个空缓冲区资源
}