经典进程同步问题
对于信号量与PV原语的总结以及应用,分析了如何使用PV原语解决一些经典的进程同步问题
在聊进程同步问题前,先弄清楚一些基础概念
1.信号量
(1)信号量的基本概念
信号量表示物理资源的实体,是一个与队列有关的整形变量。具体实现是一个变量类型,用一个记录型数据结表示,有两个分量:信号量的值, 信号量队列指针。主要作用是封锁临界区,进程同步, 维护资源计数。
typedef struct semaphore {
int value;
struct pcb *list;
}
(2)信号量的分类
信号量按用途可分为两种:公用信号量和私有信号量。公用信号量联系一组并发进程,相关进程均可在此信号量上执行PV操作,初值置为1,用于实现进程互斥,例如互斥锁mutex;私有信号量,同样用于联系一组并发进程,仅在此信号量拥有的进程执行P操作,而其他相关进程可在其上执行V操作,初值往往为0或1,用于解决进程互斥。
信号量取值可分为两种:二值信号量与一般信号量。前者用于解决进程互斥问题,后者用于解决进程同步问题
2. PV原语
(1)P原语
P是荷兰语Proberen(测试)的首字母。为阻塞原语,负责把当前进程由运行状态转换为阻塞状态,直到另外一个进程唤醒它。操作为:申请一个空闲资源(把信号量减1),若成功,则退出;若失败,则该进程被阻塞;
P原语操作的动作是:
(1) sem减1;
(2) 若sem减1后仍大于或等于零,则进程继续执行;
(3) 若sem减1后小于零,则该进程被阻塞后进入与该信号相对应的队列中,然后转进程调度。
void P(semaphore s){
s.value --; /*信号量减1*/
if(s.value < 0) /*若信号量小于0,则阻塞,s信号量状态被
sleep(s.list); /*移入s信号量队列,转向进程调度程序*/
}
(2)V原语
V是荷兰语Verhogen(增加)的首字母。为唤醒原语,负责把一个被阻塞的进程唤醒,它有一个参数表,存放着等待被唤醒的进程信息。操作为:释放一个被占用的资源(把信号量加1),如果发现有被阻塞的进程,则选择一个唤醒之。
V原语操作的动作是:
(1) sem加1;
(2) 若相加结果大于零,则进程继续执行;
(3) 若相加结果小于或等于零,则从该信号的等待队列中唤醒一等待进程,然后再返回原进程继续执行或转进程调度。
void V(semaphore s){
s.value ++; /*信号量加1*/
if(s.value <= 0) /*若信号量小于等于0,则调用wakeup(s.list)*/
wekeup(s.list); /*从信号量s队列中释放一个等待信号量的进程并*/
/*转换为就绪态,进程继续执行*/
}
(3)PV原语对信号量的操作
PV原语对信号量的操作可分为三种情况:
**1)**把信号量视为一个加锁标志位,实现对一个共享变量的互斥访问(PV原语实现互斥)
实现过程:
P(mutex) ; // mutex 的初始值为1,访问该共享数据
V(mutex) ;
非临界区
当有进程在临界区中时,mutex的值为0或复制,否则mutex值为1,因为只有一个进程,可用P操作把mutex减至为0,故可保证互斥操作。这是视图进入临界区的其他进程会因执行P(mutex)而被迫等待。mutex的取值范围时 1 − ( n − 1 ) 1 ~ -(n - 1) 1 −(n−1),表明有一个进程在临界区执行,最多有 n − 1 n-1 n−1个进程在信号量队列中等待。
2)把信号量视为是某种类型的共享资源的剩余个数,实现对一类共享资源的访问
实现过程:
P(resource); // resource的初始值为该资源的个数N使用该资源;
V(resource);
非临界区
3)把信号量作为进程间的同步工具
实现过程:
临界区C1;
P(S);
V(S);
临界区C2;
3. PV原语解决进程同步问题
(1)生成者-消费者问题
问题描述
这是一个著名的进程同步问题。它描述的是:有一群生产者进程在生产产品,并将这些产品提供给消费去消费。为使生产者进程与消费者进程能并发执行,在两者之间设置了一个具有N个缓冲区的环形缓冲池,生产者进程将它所生产的产品放入一个缓冲区中;消费者进程可从缓冲区中取走产品去消费。
不允许消费进程到一个空缓冲区去取产品;也不允许生产者进程向一个已装满产品且尚未被取走的缓冲区中投放产品。
缓冲池
上图是生产者-消费者问题环形缓冲池
它们应满足如下同步条件:
① 不能向满缓冲区存产品
② 不能向空缓冲区取产品
③ 每个时刻仅允许一个生产者或消费者取一个产品
问题剖析
- 设缓冲区的编号为 0 − N − 1 0 - N-1 0−N−1,in和out分别是生产者进程和消费者进程使用的指针,指向下面可用的缓冲区,初值都是0
- 设置三个信号量:
- full:表示放有产品的缓冲区数,其初值为0
- empty:表示可供使用的缓冲区数,其初值为N
- mutex:互斥信号量,初值为1,表示各个进程互斥进入临界区,保证如何时候只有一个进程使用缓冲区
/*m个生产者和n个消费者共享k件产品缓冲区问题*/
intem B[k];
semaphore empty; empty = N;
semaphore full; full = 0;
semaphore mutex; mutex = 1;
int i = 0; // 放入缓冲区指针
int out = 0; // 取出缓冲区指针
// 生产者进程Producer
process producer_i(){
while(true){
produce();
P(empty);
P(mutex);
产品送往buffer(in);
in = (in + 1) mod N;
V(mutex);
V(full);
}
}
// 消费者进程Consumer
process consumer_j(){
while(true){
P(full);
P(mutex);
从buffer(out)中取出产品;
out = (out + 1) mod N;
V(mutex);
V(empty);
}
}
程序中的P(mutex)和V(mutex)必须要成对出现,夹在两者之间的代码段是临界区;信号量empty和full的PV操作也必须要成对出现,但分别位于不同的程序中。
在这个问题中,P操作的次序十分重要,如果把生产者进程中的两个P操作互换顺序,那么当缓冲区中存满N件商品时(也就是empty=0, mutex=1, full=N), 生产者又生产一件产品,在它将要向缓冲区存放时,将在P(empty)上等待,由于此时mutex=0,缓冲区已经被占用。所以当消费者将停留在P(mutex)而无法取到产品。这就使得生产者在等待消费者取走产品,消费者在等待生产者释放缓冲区占有权。也就是死锁。
所以在使用PV操作实现进程同步时,要注意P操作的次序,而V操作的次序无关紧要。
一般来说,用于互斥的信号量上的P操作总是在后面执行。
(2)读者-写者问题
问题描述
读者写者问题也是一个经典的并发程序设计问题,此问题类似文件读写的逻辑,一个数据文件或记录可被多个进程共享。有两组并发进程:reader和writer
要求
1. 允许多个读者同时对文件执行读操作
2. 只允许一个写者对文件执行写操作
3. 任何写者在完成写操作前不允许其他读者或写者工作
4. 写者在执行写操作前,需要保证已有的写者和读者全部退出
问题剖析
- 设置信号量
- wmutex:互斥量,写者与其他读者/写者互斥地访问
- mutex:互斥量,互斥访问临界资源readcount,初值为1
- readcount:读者计数,初值为0
// 读者Readers
int readcount = 0;
semaphore wmutex, mutex;
wmutex = 1, mutex = 1;
cobegin
process readers(){
while(true){
1 P(mutex); // 保护临界资源,避免多个读者同时读入,造成计数混乱,形成死锁
2 if(readcount == 0) P(wmutex); // 只有当第一个读者进来时才需要判断写者是否在运行状态
3 ++ readcount;
4 V(mutex);
5 reading;
6 P(mutex);
7 readcount --;
8 if(readcount == 0) V(wmutex);
9 V(mutex);
}
}
coend
// 写者writer
process writer(){
while(true){
1 P(wmutex);
2 writing;
3 V(wmutex);
}
}
举个例子
如果有五个进程 r 1 r_1 r1, w 1 w_1 w1, r 2 r_2 r2, w 2 w_2 w2, r 3 r_3 r3依次进入读写同一个文件, r i j r_{ij} rij表示第 i i i个进程在第 j j j个步骤上执行
步骤 | wmutex | mutex | readcount | 描述 |
---|---|---|---|---|
初 | 1 | 1 | 0 | 初始态 |
r 11 r_{11} r11 | 0 | 读者互斥量– | ||
r 12 r_{12} r12 | 0 | 第一个读者进入,后面写者都会被阻塞了 | ||
w 11 w_{11} w11 | -1 | 写者1进入阻塞队列 | ||
r 21 r_{21} r21 | -1 | 读者2进入阻塞队列 | ||
w 21 w_{21} w21 | -2 | 写者2进入阻塞队列 | ||
r 31 r_{31} r31 | -2 | 读者3进入阻塞队列 | ||
r 13 r_{13} r13 | 1 | 计数++ | ||
r 14 r_{14} r14 | -1 | 唤醒等待(阻塞)队列中的一个进程(读者2) | ||
r 22 r_{22} r22 | 读者2判断后知道无须在占用阻塞写者 | |||
r 15 r_{15} r15 | 读者1reading… | |||
r 23 r_{23} r23 | 2 | 计数++ | ||
r 24 r_{24} r24 | 0 | 唤醒等待(阻塞)队列中的一个进程(读者3) | ||
r 32 r_{32} r32 | 读者3判断后知道无须在占用阻塞写者 | |||
r 15 r_{15} r15 | 读者1reading… | |||
r 25 r_{25} r25 | 读者2reading… | |||
r 33 r_{33} r33 | 计数++ | |||
r 15 r_{15} r15 | 读者1reading… | |||
r 25 r_{25} r25 | 读者2reading… | |||
r 34 r_{34} r34 | 1 | 结果大于零,则进程(读者3)继续执行 | ||
r 15 r_{15} r15 | 读者1reading… | |||
r 25 r_{25} r25 | 读者2reading… | |||
r 35 r_{35} r35 | 读者3reading… | |||
r 36 r_{36} r36 | 0 | 读者3准备要离开(进程即将结束),执行P原语 | ||
r 16 r_{16} r16 | -1 | 读者1准备要离开(进程即将结束) | ||
r 26 r_{26} r26 | -2 | 读者2准备要离开(进程即将结束) | ||
r 37 r_{37} r37 | 2 | 计数– | ||
r 38 r_{38} r38 | 不是最后一个读者,还无法唤醒写者进程 | |||
r 39 r_{39} r39 | -1 | 离开,执行V原语 | ||
r 17 r_{17} r17 | 1 | 计数– | ||
r 18 r_{18} r18 | 不是最后一个读者,还无法唤醒写者进程 | |||
r 19 r_{19} r19 | 0 | 离开,执行V原语 | ||
r 27 r_{27} r27 | 0 | 计数– | ||
r 28 r_{28} r28 | -1 | 最后一个读者,等待(阻塞)队列中的一个进程(写者1) | ||
w 12 w_{12} w12 | 写者writing… | |||
w 29 w_{29} w29 | 1 | 离开,执行V原语 | ||
w 13 w_{13} w13 | 0 | 写者1离开 | ||
w 22 w_{22} w22 | 由于wmutex>=0,写者2也可以writing | |||
w 23 w_{23} w23 | 1 | 写者2离开 |