对于信号量的使用,主要是如何选择信号量和如何安排pv操作在程序中的位置。下面来分析一下经典的生产者消费者问题。教材的说法有点过于笼统了,我就用自己的语言来重新描述一遍。
生产者——消费者问题
问题描述:有两组进程共享一个环形的缓冲池,其中的一组进程称为生产者,另一组进程称为消费者。缓冲池由若干个大小相等的缓冲区组成,每一个缓冲区都可以容纳一个产品。生产者进程不断的将生产的产品放入到缓冲池中,消费者进程则不断的将产品从缓冲池中取出。指针 i 和 j 分别指向第一个空闲的缓冲区和第一个装满的缓冲区。(阴影部分表示满,空白部分表示空)
问题分析
解决这类问题,关键是要理清楚,谁是临界资源?进程间存在什么交互关系?应该如何设置信号量?如何安排PV操作的顺序。
- 临界资源分析:缓冲池中的缓冲区是临界资源(就是图中一小块一小块的东西),它一次只允许一个生产者放入产品或者一个消费者从中取出产品消费。
- 交互关系分析:生产者访问缓冲区的时候,消费者不能访问,当然,生产者之间也不能访问同一个缓冲区。反之亦然,故而两者间存在访问缓冲区时,存在互斥的关系,但是生产者必须在消费者之前进行(不然你消费什么),因此也存在同步关系。
- 思路分析:这里面只有两种进程,关系上只有同步和互斥,因此只要PV操作并合理安排其位置就可以解决此类问题。
- 信号量设置:因为存在互斥的问题,所以先设置互斥信号量 mutex = 1。用于控制两个进程对缓冲区的互斥访问,再设置一个信号量full,用于记录当前缓冲池中已经满的缓冲区数,刚刚开始的时候,生产者还没有生产,因此初始值为0.而信号量empty,用来记录当前缓冲区中空的缓冲区数,初始值为n,于是我们很容易看出来,full + empty = n始终成立。
生产者——消费者伪代码分析如下:
//生产者消费者问题
//设置信号量的初始值
semaphore mutex = 1;
semaphore empty = n,full = 0;
int i,j;//设置指针
//生产者进程
void producer(){
while(true){
生产一个数据;
P(empty);//申请一个空白的区域用来存放生产的数据,此时empty - 1
//申请完了之后,互斥进入缓冲区,使得其他进程不能访问该缓冲区
p(mutex);//mutex = mutex - 1 = 0
将数据放入缓冲区;
V(mutex);//退出缓冲区,互斥信号量恢复为1(即mutex = mutex+1)
V(full);//此时,缓冲池中的满的缓冲区 +1
//对于消费者来说,这就是释放了资源
}
}
void consumer(){
while(true){
P(full);//申请一块满的缓冲区
P(mutex);//互斥进入
将数据从缓冲池中取出来;
V(mutex);//互斥退出
V(empty);//释放缓冲区,此时缓冲区状态为空
//缓冲池中的空的缓冲区 +1
消费取出的数据。
}
}
这里注意一下,消费者进程是先取出来再消费的。我们称用来表示互斥的信号量为互斥信号量,代表可使用的资源量称为资源信号量,那么我们应该先对资源信号量进行P操作后,才能对互斥信号量进行P操作。
那如果我们反起道而行之呢?分析一下。先执行p(mutex)再执行P(empty);。倘若此时,生产者已经将缓冲池放满,消费者并没有来取产品(即empty = 0);下次仍然是生产者运行,它先执行p(mutex),被阻塞,希望消费者取出产品后将其唤醒,但是这个时候,由于先执行的是p(mutex),mutex的值为0,信号量被封锁,消费者进程进不去临界区,因而被阻塞。这样双方都指望对方唤醒自己,然后又都陷入阻塞。因而陷入无休止的等待。这种状态我们称为死锁。(后面详细介绍)
读者——写者问题
问题描述:一个数据文件被多个并发进程所共享,其中一些进程只要求读取文件的内容,而一些进程则要求对文件内容进行修改。我们称前者为读者,后者为写者。因为读者并不改变文件的内容,所以我们允许多个进程同时访问,而写者不同,他们要改变数据对象中的内容,因此一只能允许一个写者进程,并且写的时候也不能有读者进程进行读取。我们把限制条件罗列一下:
- 允许任意多个进程同时进行读操作
- 一次只能允许一个写进程进行写操作
- 若有一个写进程进行写,那么所有进程都不能对文件操作(包括读)
- 写者在执行写操作的时候,应该要求已经在对文件进行操作的读,或者写进程退出。
问题分析
- 临界资源分析:显然被访问的文件是临界资源
- 交互关系分析:写者与所有的进程都互斥,读者与读者都不互斥
- 思路分析:对于写者而言,它与所有的进程都互斥,用简单的PV操作就可以完成。但是读者的问题较为复杂,它除了要与写者之间实现互斥,还要实现与其他写者的同步(因为写者要在读者离开以后才能写)。那么,设置一个计数器,用来判断当前是否有读者在读文件。有则加1,那么这个变量对于读者来说是个共享的变量,人人都可以访问,所以读者间也要互斥的访问。
- 信号量的设置:现在很明确,我们要设置信号量Rcount,用来记录当前读者的数量,初始值为0,设置Rmutex为互斥信号量,初始值为1,用于读者之间对计数器的互斥访问,设置Wmutex,用于写者之间的互斥访问,初始值为1。
实现的伪代码如下:
semaphore Wmutex,Rmutex = 1;//互斥信号量
int Rcount = 0;
//读者进程
void reader(){
while(true){
P(Rmutex);//读者进程互斥访问计数器变量
//如果此时没有读者进程,那么先申请Wmutex,使它 =0
if(Rcount == 0) P(Wmutex);//这样所有的写进程都被阻塞
Rcount ++;//否则读者进程数 +1,表明现在多了一个读者进程
V(Rmutex);//释放计数器信号量
..............
...读取文件...
..............
P(Rmutex);//每次访问Rcount都是以互斥的形式
Rcount--;//读完文件后,进程退出,计数器减一
//如果此时没有读者进程,那么唤醒写者进程,允许写
if(Rcount == 0)V(Wmutex);
V(Rmutex);//释放互斥信号量
}
}
//写者进程
void writer(){
while(true){
P(Wmutex);
............
..写入文件..
............
V(Wmutex)
}
}
上面的代码是一种读者优先的策略。那么哪里体现了优先呢?我们看看读者进程中的这两句代码:
P(Rmutex);//读者进程互斥访问计数器变量
//如果此时没有读者进程,那么先申请Wmutex,使它 =0
if(Rcount == 0) P(Wmutex);//这样所有的写进程都被阻塞
若读者计数器为0,那么这时候可能有这么一种情况,读者写者进程同时要求访问这个文件,但是他们不能同时访问,因为读的时候不能写,写的时候不能读。所以,上面的做法是 P(Wmutex),将写者进程的信号量申请掉,也就是Wmutex = 0,那么写者进程由于再P的话就会进入阻塞状态。相当于让步给读者进程进行操作。带操作完成后再V(Wmutex),唤醒写者进程进行操作。
因此我们换个角度,也可以让写者优先,我就具体的不说了,原理同上。
但是注意:无论是读者优先还是写者优先,当优先级较高的进行操作的时候,那么优先级较低的就必须等待。如果后面来的都是优先级较高的操作,那么致歉优先级较低的就会被无限期的挂起,造成饥饿现象。
于是一种公平策略的做法就产生了,具体的我就不写了,写个规则,下篇文章贴出来。
- 在一个读序列中国,若有写者等待,那么就不允许新来的读者开始执行
- 在一个写操作结束的时候,所有等待的读者必须比下一个写者有更高的优先权。