PV操作经典问题包括:读者写者问题,生产者消费者问题,哲学家就餐问题,理发师问题。
下面先给出解题通用步骤,再对每一类问题做例题分析,并给出每类问题的特解。
1.有几类进程?每类进程对应一个函数。
如
生产者1进程对应producer1()
消费者1进程对应consumer1()
消费者2进程对应consumer2()
2.在函数内部用中文描述动作,并在草纸上行间留白供后续使用,并根据这些动作是否重复进行,决定是否加while(1)
如
producer{
while(1){
生产一件商品;
把商品放到货架;
}
}
3.在每个动作之前,思考是否需要P什么?如果需要P操作,写出P操作,并写出对应的V操作(V操作可能在另一进程内)
如
producer{
while(1){
//不需要P
生产一件商品;
//由于需要消耗货架容量,所以需要P(货架空闲区)
//货架属于临界资源需要互斥访问,所以P(货架互斥信号量)
把商品放到货架;
//P的对应V操作:释放临界资源,所以V(货架互斥信号量)
//由于货架被放上货物,货物量增加,所以V(货架货物量)
4.写完PV操作后,再完整地定义信号量
如
semaphore mutex=1;//临界资源
semaphore empty=5//空闲区数量
semaphore full=0//货物数量
int count=0;//整形信号量,用于统计人数等
semaphore CSignal;//在count++ count————时,套上P()V(),实现互斥更改count值
5.完成上述步骤之后进行检查,检查多个P操作连续出现时是否可能出现死锁。一对PV前后出现,不会死锁;连续多个P导致死锁(请求和保持),可尝试调换前后P的顺序。
例一用于实操一遍通解过程的1,2,3,4,5步,例二用于说明第5步会遇到的问题。
例一:某工厂有两个生产车间和一个装配车间,两个生产车间分别生产 A、B 两种零件,装配车间的任务是把 A、B 两种零件组装成产品。两个生产车间每生产一个零件后,都要分别把它们送到装配车间的货架 F1、F2上。F1 存放零件 A,F2 存放零件 B,F1 和 F2 的容量均可存放 10 个零件。装配工人每次从货架上取一个零件 A 和 一个零件 B 后组装成产品。请用 P、V 操作进行正确管理。
思考步骤,解体流程:
1.有几类进程?每类进程对应一个函数。
如
生产者A进程对应producerA()
生产者B进程对应producerB()
消费者进程对应consumer()
2.在函数内部用中文描述动作,并在草纸上行间留白供后续使用,并根据这些动作是否重复进行,决定是否加while(1)
如
producerA{
while(1){
生产一件零件A;
把零件A放到货架F1;
}
}
producerB{
while(1){
生产一件零件B;
把零件B放到货架F2;
}
}
consumer{
while(1){
从货架F1取一件零件A;
从货架F2取一件零件B;
把零件A和B组装;
}
}
3.在每个动作之前,思考是否需要P什么?如果需要P操作,写出P操作,并写出对应的V操作(V操作可能在另一进程内)
如
producerA{
while(1){
生产一件零件A;
P(emptyF1)//由于需要消耗货架容量,所以需要P(货架空闲区)
P(mutexF1)//货架属于临界资源需要互斥访问,所以P(货架互斥信号量)
把零件A放到货架F1;
V(mutexF1);//P的对应V操作:释放临界资源,所以V(货架互斥信号量)
V(fullF1)//由于货架被放上货物,货物量增加,所以V(货架货物量)
}
}
producerB{
while(1){
生产一件零件B;
P(emptyF2)//由于需要消耗货架容量,所以需要P(货架空闲区)
P(mutexF2)//货架属于临界资源需要互斥访问,所以P(货架互斥信号量)
把零件B放到货架F2;
V(mutexF2);//P的对应V操作:释放临界资源,所以V(货架互斥信号量)
V(fullF2)//由于货架被放上货物,货物量增加,所以V(货架货物量)
}
}
consumer{
while(1){
P(fullF1)//由于需要消耗货物A,所以需要P(货架货物量)
P(mutexF1)//货架属于临界资源需要互斥访问,所以P(货架互斥信号量)
从货架F1取一件零件A;
V(mutexF1);//P的对应V操作:释放临界资源,所以V(货架互斥信号量)
V(emptyF1)//取完货物,货架F1空闲区增加,所以V(货架空闲区)
P(fullF2)//由于需要消耗货物B,所以需要P(货架货物量)
P(mutexF2)//货架属于临界资源需要互斥访问,所以P(货架互斥信号量)
从货架F2取一件零件B;
V(mutexF2);//P的对应V操作:释放临界资源,所以V(货架互斥信号量)
V(emptyF2)//取完货物,货架F2空闲区增加,所以V(货架空闲区)
把零件A和B组装;
}
}
4.写完PV操作后,再完整地定义信号量
如
semaphore mutexF1=1;//临界资源
semaphore mutexF2=1;
semaphore emptyF1=10;//空闲区数量
semaphore emptyF2=10;
semaphore full1F=0;//货物数量
semaphore full2F=0;
5.完成上述步骤之后进行检查,检查多个P操作连续出现时是否可能出现死锁。一对PV前后出现,不会死锁;连续多个P导致死锁(请求和保持),可尝试调换前后P的顺序。
未发现死锁现象,完整答案:
semaphore mutexF1=1;//临界资源
semaphore mutexF2=1;
semaphore emptyF1=10;//空闲区数量
semaphore emptyF2=10;
semaphore full1F=0;//货物数量
semaphore full2F=0;
producerA{
while(1){
生产一件零件A;
P(emptyF1)//由于需要消耗货架容量,所以需要P(货架空闲区)
P(mutexF1)//货架属于临界资源需要互斥访问,所以P(货架互斥信号量)
把零件A放到货架F1;
V(mutexF1);//P的对应V操作:释放临界资源,所以V(货架互斥信号量)
V(fullF1)//由于货架被放上货物,货物量增加,所以V(货架货物量)
}
}
producerB{
while(1){
生产一件零件B;
P(emptyF2)//由于需要消耗货架容量,所以需要P(货架空闲区)
P(mutexF2)//货架属于临界资源需要互斥访问,所以P(货架互斥信号量)
把零件B放到货架F2;
V(mutexF2);//P的对应V操作:释放临界资源,所以V(货架互斥信号量)
V(fullF2)//由于货架被放上货物,货物量增加,所以V(货架货物量)
}
}
consumer{
while(1){
P(fullF1)//由于需要消耗货物A,所以需要P(货架货物量)
P(mutexF1)//货架属于临界资源需要互斥访问,所以P(货架互斥信号量)
从货架F1取一件零件A;
V(mutexF1);//P的对应V操作:释放临界资源,所以V(货架互斥信号量)
V(emptyF1)//取完货物,货架F1空闲区增加,所以V(货架空闲区)
P(fullF2)//由于需要消耗货物B,所以需要P(货架货物量)
P(mutexF2)//货架属于临界资源需要互斥访问,所以P(货架互斥信号量)
从货架F2取一件零件B;
V(mutexF2);//P的对应V操作:释放临界资源,所以V(货架互斥信号量)
V(emptyF2)//取完货物,货架F2空闲区增加,所以V(货架空闲区)
把零件A和B组装;
}
}
例二:某寺庙有小和尚、老和尚若干,有一水缸,由小和尚提水入缸供老和尚引用。水缸可容 10 桶水,水桶自同一井中。水井径窄,每次只能容一个桶取水。水桶总数为 3 个。每次入缸取水仅为 1 桶水,且不可同时进行。试给出有关从缸取水、入水的算法描述。
先给出正确解答,再说明第5步可能出现的问题。
semaphore well = 1; //用于互斥地访问水井;
semaphore vat = 1; //用于互斥的访问水缸;
semaphore empty = 10; //用于表示水缸中剩余空间;
semaphore full = 0; //表示水缸中水的桶数;
semaphore pail = 3; //表示有多少个水桶可以用,初始值为3;
// 老和尚
while(1){
P(full);
P(pail);
P(vat);
从水缸中打一桶水;
V(vat);
V(empty);
喝水;
V(pail);
}
// 小和尚
while(1){
P(empty);
P(pail);
P(well);
从井中打一桶水;
V(well);
P(vat);
将水倒入水缸中;
V(vat);
V(full);
V(pail);
}
我们在写P操作时,遇到的多个P操作,应该先写哪个P呢?我给出策略1可以把互斥访问临界资源的P(mutex)放在最后,而前面的多个P可能不确定,假如老和尚写成
P(pail);//取桶
P(full);//确定有水
P(vat);//互斥缸
从水缸中打一桶水;
而此时缸中无水,且有3个和尚共取了3个桶,那么小和尚无法取桶去取水,老和尚就永远喝不到水,产生死锁。
这时我们就应该调换P操作顺序为
P(full);
P(pail);
P(vat);
从水缸中打一桶水;
那么我们就会产生疑问,有没有一种思考方式来直接避免多个P出现死锁呢?下面我给出一种策略2
如果我们要做两件事,且先做A,再做B,那么我们就应该先确保做B所需资源存在,再确定做A所需资源存在。
比如老和尚要喝水,两件事:先取桶,再从缸里取水,那么我们就应该先确定缸里水存在,再确定桶存在。用PV描述:
P(full);确定缸里水存在
P(pail);确定桶存在
再比如小和尚要取水,两件事:先从用桶从井里取水,再把水倒入缸中空闲区,那么我们就应该先确定缸中空闲区存在,再确定桶存在。用PV描述:
P(empty);
P(pail);
对读者优先,读写公平,写者优先三种策略,我们按照通解写出答案,再对特殊部分进行解读。
按照通解得到:
semaphore RCsignal=1;//读者数修改互斥
semaphore mutex=1;//临界资源互斥
int count=0;//读者数
读者部分:
reader(){
while(1){
P(mutex);
读;
V(mutex);
}
}
写者部分:
writer(){
while(1){
P(mutex);
写;
V(mutex);
}
}
使用了整形信号量readcount,目的是在第一个读者进入临界资源时,锁住临界资源,使后续读者可以继续访问而后续写者无法访问;在最后一个读者撤出临界资源时,解锁临界资源,使后续读者或写者都可访问。
semaphore RCsignal=1;//读者数修改互斥
semaphore mutex=1;//临界资源互斥
int count=0;//读者数
读者部分:
reader(){
while(1){
P(RCsignal);
if(count==0)
P(mutex);
V(RCsignal);
读;
P(RCsignal);
count--;
if(count==0)
V(mutex);
V(RCsignal);
}
}
写者部分:
writer(){
while(1){
P(mutex);
写;
V(mutex);
}
}
读写公平分为两种,一种是部分自行抢占部分先来先服务,一种是先来先服务。
两者的PV操作区别是写者中P(rw)的位置不同。
书中,博客中所给的基本都是第一种读写公平,个人认为第二种是更公平的写法。
部分自行抢占,部分先来先服务:写者A在访问临界资源时,又先后来了B,C,那么B,C有同等概率先访问临界资源;读者A在访问临界资源时,又
先后来了B,C,那么B将优先于C先访问临界资源。
部分自行抢占部分先来先服务的具体机制
部分自行抢占部分先来先服务{
目前临界资源正被某读者A访问{
若此时先后来到一个读者B,一个写者C{
B与A同时读;
}
若此时先后来到一个写者B,一个读者C{
A出后,B写;
}
目前临界资源正被某写者A访问{
若此时先后来到一个读者B,一个写者C{
A出后,BC有同等概率抢占临界资源;
}
若此时先后来到一个写者B,一个读者C{
A出后,BC有同等概率抢占临界资源;
}
}
如何实现这个机制呢?
我们引入一个信号量rw,当A在读时,来B,B即获得下一位进入临界区的资格。所以B(无论时读写)应首先P(rw),获得资格,此时若再来C,则C被P(rw)阻塞,即C无法获得资格。
若B为写,已经获得资格,那么B什么时候再释放这个资格呢?答案是当B写完,B写完后,后续来者自行抢占。
若B为读,已经获得资格,那么B什么时候再释放这个资格呢?答案是当B在读之前,因为读时B已经像A那样允许下一位获得资格。
semaphore mutex=1;
semaphore RCsignal=1;
semaphore rw=1;
int count=0;
读者部分:
reader(){
while(1){
P(rw);
P(RCsignal);
if(count==0)
P(mutex);
count++;
V(RCsignal);
V(rw);
读;
P(RCsignal);
count--;
if(count==0)
V(mutex);
V(RCsignal);
}
}
写者部分:
writer(){
while(1){
P(rw);
P(mutex);
写;
V(mutex);
V(rw);
}
}
先来先服务:简单来说A在访问临界资源时,又先后来了B,C,那么B将优先于C先访问临界资源。
先来先服务的具体机制:
先来先服务{
目前临界资源正被某读者A访问{
若此时先后来到一个读者B,一个写者C{
B与A同时读;
}
若此时先后来到一个写者B,一个读者C{
A出后,B写;
}
目前临界资源正被某写者A访问{
若此时先后来到一个读者B,一个写者C{
A出后,B读;
}
若此时先后来到一个写者B,一个读者C{
A出后,B写;
}
}
如何实现这个机制呢?
我们引入一个信号量rw,当A在读或写时,来B,B即获得下一位进入临界区的资格。所以B(无论时读写)应首先P(rw),获得资格,此时若再来C,则C被P(rw)阻塞,即C无法获得资格。
B已经获得资格,那么B什么时候再释放这个资格呢?答案是当B在读写之前,因为读写时B已经像A那样允许下一位获得资格。
semaphore mutex=1;
semaphore RCsignal=1;
semaphore rw=1;
int count=0;
读者部分:
reader(){
while(1){
P(rw);
P(RCsignal);
if(count==0)
P(mutex);
count++;
V(RCsignal);
V(rw);
读;
P(RCsignal);
count--;
if(count==0)
V(mutex);
V(RCsignal);
}
}
写者部分:
writer(){
while(1){
P(rw);
P(mutex);
V(rw);
写;
V(mutex);
}
}
写者优先的规则:
1.读者与写者,写者与写者不能同时访问缓冲区;
2.无写进程时,各读者可同时访问缓冲区;
3.读者和写者都等待时,写者优先访问缓冲区;
“读者和写者都等待时,写者优先访问缓冲区”解读
读者写者都等待的情况,即有写者正在访问临界资源,而不可能是有读者正在访问临界资源,这是因为有读者在访问临界资源时,后到的写者 只能等待访问临界资源的读者撤出后 才能访问临界资源(读者写者问题的基本原则),而后到的读者可以与前面的读者共同访问临界资源。
写优先代码解读
在读者写者都等待的情况,即有写者正在访问临界资源时,write=0(代码中写了,当访问临界资源的写者撤出时,Wcount=0,才会V(write),使write=0),假如此时按时间顺序又来了读者A,写者B。此时在write=0的情况下,按时间顺序读者A先执行P(write),即读者A被阻塞;然后按时间顺序写者B执行,由于此时Wcount=1,即P(write)语句不会执行,即不会在此阻塞,继续执行直到P(mutex)被阻塞。目前读者A与写者B都处于阻塞状态。但一旦正在访问临界资源的写者退出并执行V(mutex)后,使得mutex=1,并且写者B得知mutex=1,并执行P(mutex),进而进行“写”。A退出时,Wcount=1,因此不会V(write),因此读者B将继续被阻塞。
此时即实现了,在读者写者都等待时,即有写者正在访问临界资源的情况下,按时间顺序又来了读者A,一个写者B,而执行结果就是,读者A被阻塞,写者B也被阻塞。但一旦正在访问临界资源的写者退出并执行V(mutex)后,写者B进行“写”,即写者B先于读者A,实现了插队。即读者和写者都等待时,写者优先访问缓冲区。
semaphore write=1;//进程优先互斥
semaphore mutex=1;//临界资源互斥
semaphore RCsignal=1;//读者数Rcount修改互斥
semaphore WCsignal=1;//写者数Wcount修改互斥
int Rcount=0;//读者数
int Wcount=0;//写者数
读者部分:
reader(){
while(1){
P(write);
P(RCsignal);
if(Rcount==0)
P(mutex);
Rcount++;
V(RCsignal);
V(write);
读;
P(RCsignal);
Rcount--;
if(Rcount==0)
V(mutex);
V(RCsignal);
}
}
写者部分:
writer(){
while(1){
P(WCsignal);
if(WCsignal==0)
P(write);
Wcount++;
V(WCsignal);
P(mutex);
写;
V(mutex);
P(WCsignal);
Wcount--;
if(Wcount==0)
V(write);
V(WCsignal);
}
}
哲学家进餐问题的特点是只有一类进程,且进程数量小于资源数量。
解决方法是当一个进程能够得到所有需要的资源时,再一次性取走这些资源。
通解:
process(){
while(1){
P(mutex);//用于互斥访问资源
if(资源A>=a&&资源B>=b){//如果所有资源都足够
资源A--;//一次性获得全部资源
资源B--;
V(mutex);
break;//已经获得资源,跳出循环,至做事
}
else
V(mutex); //资源不够,重复查询
}
做事;//比如哲学家吃饭
P(mutex);//互斥访问资源
资源A++;//一次性退还全部资源
资源B++;
V(mutex);
}
实在复杂,这里跳向一篇好文。
链接: https://blog.csdn.net/duanzhengbing/article/details/52141699