G
N
I
D
A
O
L

【操作系统-进程】PV操作——生产者消费者问题

目录

生产者消费者问题的万能方法步骤

Step 1. 有几类进程

按题意,区分有几类进程,每类进程对应一个函数。

按题意,若进程是无限循环的,则应当加上while(1)死循环体;如果不是,则不用加。

注意,不是说题目中有几个进程就有几个进程,要按题目意思。

Step 2. 用中文描述动作

按题意,用中文描述每类进程的操作。

Step 3. 添加 PV 操作,用中文描述里面的操作

(1)添加一般的信号量

从第一个进程开始,思考里面的每一步操作之前是否需要 P 操作。若需要,则再思考对应的 V 操作需要添加到哪个地方。因为 PV 操作都是成对出现的,所以这种方法能有效避免漏操作。

一般来说,某操作之前的 P 操作都是检查缓冲区有没有空位,而对应的 V 操作则是缓冲区的空位增加。

定义信号量是在下一个步骤进行的,因此,可以先用中文描述各类 PV 操作,例如:

  • P(缓冲区有空位?若有,空位-1,若无则阻塞);
  • V(空位+1);
  • P(缓冲区有产品?若有,产品-1,若无则阻塞);
  • V(产品+1);

这样写出中文描述的好处是可以知道信号量的作用是什么,更方便我们接下来去定义信号量。

(2)添加互斥信号量

在思考完所有操作之后,还需要注意对缓冲区的互斥访问条件,即所谓的“互斥锁”,也可以先用中文描述互斥的 PV 操作,例如:

  • P(互斥锁加锁);
  • V(互斥锁解锁);

以上两个步骤都是最基本的步骤,下面的第四步需要大家认真读题并思考才能发现其中隐含的互斥条件。

Step 4. 检查是否出现死锁

(1)检查连续 P 操作是否出现死锁

这里所指的连续多个 P 操作是对一般信号量的连续 P 操作,不包括互斥信号量的 P 操作,后文均默认为此种情况。

像这种出现连续 P 操作的题目,进程所需要的资源个数一般大于等于 2。根据死锁的原理,进程获得资源就不会释放,满足请求和保持条件,因而如果两个进程的资源数不够,但是又互相拿着对方所需的资源,那么就会发生死锁。(参见题目 4)

我们要做的,是检查多个信号量的 P 操作连续出现的地方,是否可能产生死锁。解决方案是:连续 P 操作要一气呵成,需要多加一个互斥信号量,在连续 P 操作的头尾加上互斥锁。

(2)即使不是连续的 P 操作,也有可能发生死锁

这种情况发生在一个进程既需要多个资源,又需要进行多个操作的情况,因为获得资源和完成操作是交错进行的,既不能一次性获得所有资源,又不能一次性完成所有操作,这时候就容易导致死锁。(参见题目 8)

解决办法很简单,把所有申请资源的 P 操作都写在一起,转化为连续的 P 操作,这样就变成了先前所述的第一种情况,用先前所述办法解决即可。

(3)检查共享缓冲区是否有隐含的死锁

当多个进程共享同一个缓冲区时,就可能会出现其中一个进程推进过快,而导致缓冲区先行被该进程的产品所占满,其他进程无法放入产品的情况。(参见题目 6)

比如,进程 A 生产 A,进程 B 生产 B,消费者需要的资源是一个 A 和一个 B,缓冲区空位为 N。现在缓冲区已经放满了 A,消费者已经拥有了 A,还需要 B,但是缓冲区已经满了,进程 B 放不了 B,结果就发生了死锁现象。

所以,要想不死锁,就不能让其中一个进程全部占满缓冲区,进程 A 最多只能放 N-1 个 A,多了就肯定死锁;同理,进程 B 最多只能放 N-1 个 B,多了也肯定死锁。

这种问题的解决思路一般是将问题转化为两个缓冲区来解决,方法是构造两个虚拟的缓冲区,一个给进程 A 用,一个给进程 B 用,A 缓冲区最大空位为 N-1,B 缓冲区最大空位为 N-1。放产品的时候,需要同时往真实的缓冲区和虚拟缓冲区放。进程先检测真实缓冲区是否有空位;如果有,则再检测自己的虚拟缓冲区是否有空位,如果也有,才能放进真实缓冲区和虚拟缓冲区。

Step 5. 定义信号量

直接按照 PV 操作中的中文描述去定义信号量以及初始值。

对于互斥信号量,初始值一定为 1。

对于前驱后继的信号量,初始值一般为 0。

对于一般的信号量,初始值要看题意,一般来说,各个进程开始执行前的状态即为初始值。例如,在生产着消费者问题中,未运行前的缓冲区的空位数为 N,而产品数为 0,因为还没运行,还没生产出产品,缓冲区里自然也没有产品。

信号量的命名习惯一般为:

  • P(缓冲区有空位?若有,空位-1,若无则阻塞);,该信号量命名为 empty
  • V(空位+1);,该信号量命名为 empty
  • P(缓冲区有产品?若有,产品-1,若无则阻塞);,该信号量命名为 full
  • V(产品+1);,该信号量命名为 full
  • P(互斥锁加锁);,该信号量命名为 mutex
  • V(互斥锁解锁);,该信号量命名为 mutex

如果缓冲区空位数为 1,则记录该空位的信号量既是一般的信号量又是互斥信号量,可充当两个作用,这点需要注意了。所以当缓冲区为 1 时,可以只需要一个记录空位的信号量,而不需要另外的互斥信号量。

题目 1:单生产者、单消费者、单缓冲区、单次取出

【题目 1】一组生产者进程和一组消费者进程共享一个初始为空、大小为 N 的缓冲区,只有没其他进程使用缓冲区时,其中的一个进程才能访问缓冲区。对于消费者来说,只有缓冲区不空时才能访问缓冲区并读取信息;对于生产者来说,只有缓冲区不满时才能访问缓冲区并写入信息。

Step 1. 有几类进程

两类:生产者、消费者。

两类进程都是不断重复执行,因而需要加上循环体。

写出代码框架:

生产者(){
    while(1){
        
    }
}

消费者(){
    while(1){
    
    }
}

Step 2. 中文描述动作

生产者(){
    while(1){
        生产产品;
        把产品放入缓冲区;
    }
}

消费者(){
    while(1){
        从缓冲区取出产品;
        消费产品;
    }
}

Step 3. 添加 PV 操作,用中文描述里面的操作

(1)生产者进程

  • 生产产品之前不需要 P 操作。
  • 把产品放入缓冲区之前,需要先检查缓冲区是否有空位,如果有空位,那么空位 - 1,表明该产品占据一个空位;否则进程堵塞。因此需要一个 P 操作。
    • 记住,添加一个 P 操作就需要配对相应的 V 操作。跟之前的操作相反,什么地方需要空位 + 1?在消费者进程的从缓冲区取出产品之后,产品已被取出,空位 + 1,因此需要一个 V 操作。

//表示新加入的代码,下同)

生产者(){
    while(1){
        生产产品;
        P(缓冲区有空位?若有,空位-1,若无则阻塞); //
        把产品放入缓冲区;
    }
}

消费者(){
    while(1){
        从缓冲区取出产品;
        V(空位+1); //
        消费产品;
    }
}

(2)消费者进程

  • 从缓冲区取出产品之前,需要先检查缓冲区是否有产品,如果有产品,那么产品 - 1,表明该进程已经取走了一个产品;否则进程堵塞。因此需要一个 P 操作。
    • 记住,添加一个 P 操作就需要配对相应的 V 操作。跟之前的操作相反,什么地方需要产品 + 1?在生产者进程的把产品放入缓冲区之后,产品 + 1,因此需要一个 V 操作。
  • 消费产品之前不需要 P 操作。
生产者(){
    while(1){
        生产产品;
        P(缓冲区有空位?若有,空位-1,若无则阻塞);
        把产品放入缓冲区;
        V(产品+1); //
    }
}

消费者(){
    while(1){
        P(缓冲区有产品?若有,产品-1,若无则阻塞); //
        从缓冲区取出产品;
        V(空位+1);
        消费产品;
    }
}

(3)缓冲区

由于缓冲区不能同时进行读写,所以需要一个互斥锁,夹紧访问缓冲区的操作。

生产者(){
    while(1){
        生产产品;
        P(缓冲区有空位?若有,空位-1,若无则阻塞);
        P(互斥锁); //
        把产品放入缓冲区;
        V(互斥锁); //
        V(产品+1);
    }
}

消费者(){
    while(1){
        P(缓冲区有产品?若有,产品-1,若无则阻塞);
        P(互斥锁); //
        从缓冲区取出产品;
        V(互斥锁); //
        V(空位+1);
        消费产品;
    }
}

Step 4. 检查是否出现死锁

没有连续的 P 操作,因此不会发生死锁。

Step 5. 定义信号量

把所有 PV 操作的中文全部换成英文即可。由于我们写的是中文,所以需要注意 PV 操作对应的信号量是什么,以及是否相同。

信号量的初始值一般为题目中给出的初始值。互斥信号量的初始值一般为 1。

  • 空位信号量:初始值应为 N。
  • 产品信号量:初始值应为 0。
  • 互斥信号量:初始值应为 1。
信号量 empty = N;
信号量 full = 0;
信号量 mutex = 1;

生产者(){
    while(1){
        生产产品;
        P(empty);
        P(mutex);
        把产品放入缓冲区;
        V(mutex);
        V(full);
    }
}

消费者(){
    while(1){
        P(full);
        P(mutex);
        从缓冲区取出产品;
        V(mutex);
        V(empty);
        消费产品;
    }
}

题目 2:双生产者、双消费者、单缓冲区、单次取出

【题目 2】桌子上有一个盘子,每次只能向其中放入一个水果。爸爸专向盘子中放苹果,妈妈专向盘子中放橘子,儿子专等吃盘子中的橘子,女儿专等吃盘子中的苹果。只有盘子为空时,爸爸或妈妈才可向盘子中放一个水果;仅当盘子中有自己需要的水果时,儿子或女儿可以从盘子中取出。

Step 1. 有几类进程

四类:爸爸、妈妈、儿子、女儿。

四类进程都是不断重复执行,因而需要加上循环体。

写出代码框架:

爸爸(){
    while(1){
        
    }
}

妈妈(){
    while(1){
    
    }
}

儿子(){
    while(1){
        
    }
}

女儿(){
    while(1){
    
    }
}

Step 2. 中文描述动作

爸爸(){
    while(1){
        往盘子里放苹果:
    }
}

妈妈(){
    while(1){
        往盘子里放橘子;
    }
}

儿子(){
    while(1){
        从盘子取出橘子;
        吃橘子;
    }
}

女儿(){
    while(1){
        从盘子取出苹果;
        吃苹果;
    }
}

Step 3. 添加 PV 操作,用中文描述里面的操作

(1)爸爸和妈妈进程

  • 爸爸进程的放苹果之前,需要先检查盘子是否有空位,如果有空位,那么空位 - 1,表明苹果占据一个空位;否则进程堵塞。因此需要一个 P 操作。
    • 记住,添加一个 P 操作就需要配对相应的 V 操作。跟之前的操作相反,什么地方需要盘子空位 + 1?在女儿进程的从盘子取出苹果之后,苹果已被取出,盘子空位 + 1,因此需要一个 V 操作。
  • 妈妈进程的放橘子之前,需要先检查盘子是否有空位,如果有空位,那么空位 - 1,表明苹果占据一个空位;否则进程堵塞。因此需要一个 P 操作。
    • 记住,添加一个 P 操作就需要配对相应的 V 操作。跟之前的操作相反,什么地方需要盘子空位 + 1?在儿子进程的从盘子取出橘子之后,橘子已被取出,盘子空位 + 1,因此需要一个 V 操作。
爸爸(){
    while(1){
        P(盘子有空位?若有,空位-1,若无则阻塞); //
        往盘子里放苹果;
    }
}

妈妈(){
    while(1){
        P(盘子有空位?若有,空位-1,若无则阻塞); //
        往盘子里放橘子;
    }
}

儿子(){
    while(1){
        从盘子取出橘子;
        V(盘子空位+1); //
        吃橘子;
    }
}

女儿(){
    while(1){
        从盘子取出苹果;
        V(盘子空位+1); //
        吃苹果;
    }
}

(2)儿子和女儿进程

  • 儿子进程的从盘子取出橘子之前,需要先检查盘子是否有橘子,如果有橘子,那么橘子 - 1,表明该进程已经取走了一个橘子;否则,若盘子里没东西,或者装的是苹果,则进程堵塞。因此需要一个 P 操作。
    • 记住,添加一个 P 操作就需要配对相应的 V 操作。跟之前的操作相反,什么地方需要橘子 + 1?在妈妈进程的往盘子里放橘子之后,橘子 + 1,因此需要一个 V 操作。
  • 吃橘子前不需要 P 操作。
  • 女儿进程的从盘子取出苹果之前,需要先检查盘子是否有苹果,如果有苹果,那么苹果 - 1,表明该进程已经取走了一个苹果;否则,若盘子里没东西,或者装的是橘子,则进程堵塞。因此需要一个 P 操作。
    • 记住,添加一个 P 操作就需要配对相应的 V 操作。跟之前的操作相反,什么地方需要苹果 + 1?在爸爸进程的往盘子里放苹果之后,苹果 + 1,因此需要一个 V 操作。
  • 吃苹果前不需要 P 操作。
爸爸(){
    while(1){
        P(盘子有空位?若有,空位-1,若无则阻塞); 
        往盘子里放苹果;
        V(苹果+1); //
    }
}

妈妈(){
    while(1){
        P(盘子有空位?若有,空位-1,若无则阻塞);
        往盘子里放橘子;
        V(橘子+1); //
    }
}

儿子(){
    while(1){
        P(盘子是否有橘子?若有,则橘子-1,若无则堵塞); //
        从盘子取出橘子;
        V(盘子空位+1);
        吃橘子;
    }
}

女儿(){
    while(1){
        P(盘子是否有苹果?若有,则苹果-1,若无则堵塞); //
        从盘子取出苹果;
        V(盘子空位+1); 
        吃苹果;
    }
}

(3)缓冲区

盘子是个缓冲区,由于缓冲区不能同时进行读写,所以需要一个互斥锁,夹紧访问缓冲区的操作。

爸爸(){
    while(1){
        P(盘子有空位?若有,空位-1,若无则阻塞);
        P(互斥锁); //
        往盘子里放苹果;
        V(互斥锁); //
        V(苹果+1);
    }
}

妈妈(){
    while(1){
        P(盘子有空位?若有,空位-1,若无则阻塞);
        P(互斥锁); //
        往盘子里放橘子;
        V(互斥锁); //
        V(橘子+1);
    }
}

儿子(){
    while(1){
        P(盘子是否有橘子?若有,则橘子-1,若无则堵塞);
        P(互斥锁); //
        从盘子取出橘子;
        V(互斥锁); //
        V(盘子空位+1);
        吃橘子;
    }
}

女儿(){
    while(1){
        P(盘子是否有苹果?若有,则苹果-1,若无则堵塞);
        P(互斥锁); //
        从盘子取出苹果;
        V(互斥锁); //
        V(盘子空位+1);
        吃苹果;
    }
}

Step 4. 检查是否出现死锁

没有连续的 P 操作,因此不会发生死锁。

Step 5. 定义信号量

把所有 PV 操作的中文全部换成英文即可。由于我们写的是中文,所以需要注意 PV 操作对应的信号量是什么,以及是否相同。

信号量的初始值一般为初始值,即各进程未开始运行时的初始值。互斥信号量的初始值一般为 1。

  • 盘子空位信号量:初始值应为 1。
  • 苹果信号量:初始值应为 0。
  • 橘子信号量:初始值应为 0。
  • 互斥信号量:初始值应为 1。
信号量 empty = 1;
信号量 apple = 0;
信号量 orange = 0;
信号量 mutex = 1;

爸爸(){
    while(1){
        P(empty);
        P(mutex);
        往盘子里放苹果;
        V(mutex);
        V(apple);
    }
}

妈妈(){
    while(1){
        P(empty);
        P(mutex);
        往盘子里放橘子;
        V(mutex);
        V(orange);
    }
}

儿子(){
    while(1){
        P(orange);
        P(mutex);
        从盘子取出橘子;
        V(mutex);
        V(empty);
        吃橘子;
    }
}

女儿(){
    while(1){
        P(apple);
        P(mutex);
        从盘子取出苹果;
        V(mutex);
        V(empty);
        吃苹果;
    }
}

然而,我们之前已经提及,当一般的信号量的初始值为 1 时,该信号量又可以充当互斥信号量。信号量 empty 恰好符合这种情况,因此上面答案中的互斥信号量 mutex 其实是多余的。不过多添加一个互斥信号量也没有错,不确定的时候还是写上吧,以防万一。

信号量 empty = 1;
信号量 apple = 0;
信号量 orange = 0;

爸爸(){
    while(1){
        P(empty);
        往盘子里放苹果;
        V(apple);
    }
}

妈妈(){
    while(1){
        P(empty);
        往盘子里放橘子;
        V(orange);
    }
}

儿子(){
    while(1){
        P(orange);
        从盘子取出橘子;
        V(empty);
        吃橘子;
    }
}

女儿(){
    while(1){
        P(apple);
        从盘子取出苹果;
        V(empty);
        吃苹果;
    }
}

题目 3:双生产者、单消费者、双缓冲区、单次取出

【题目 3】工厂有两个生产车间和一个装配车间,两个车间分别生产 A、B 两种零件,装配车间的任务是把 A、B 两种零件组装成产品。两个生产车间每生产一个零件都要分别把它们送到装配车间的货架 F1 和 F2 上,F1 存放 A,F2 存放 B,F1 和 F2 均只能容纳一个零件。每当能从货架上取到一个 A 和一个 B 后就可以组装成一件产品。整个过程是自动进行的,使用 P、V 操作进行管理,使各车间相互合作、协调工作。

Step 1. 有几类进程

三类:A 生产车间、B 生产车间、装配车间。

三类进程都是不断重复执行,因而需要加上循环体。

写出代码框架:

A生产车间(){
    while(1){
        
    }
}

B生产车间(){
    while(1){
    
    }
}

装配车间(){
    while(1){
    
    }
}

Step 2. 中文描述动作

A生产车间(){
    while(1){
        生产零件A;
        把零件A放到货架F1上;
    }
}

B生产车间(){
    while(1){
        生产零件B;
        把零件B放到货架F2上;
    }
}

装配车间(){
    while(1){
        从货架F1上取出零件A;
        从货架F2上取出零件B;
        零件A和零件B组装产品;
    }
}

Step 3. 添加 PV 操作,用中文描述里面的操作

(1)A 生产车间进程

  • 生产零件A之前不需要 P 操作。
  • 把零件A放到货架F1上之前,需要先检查货架 F1 是否有空位,如果有空位,那么空位 - 1,表明该零件占据一个空位;否则进程堵塞。因此需要一个 P 操作。
    • 记住,添加一个 P 操作就需要配对相应的 V 操作。跟之前的操作相反,什么地方需要空位 + 1?在装配车间进程的从货架F1上取出零件A之后,零件已被取出,空位 + 1,因此需要一个 V 操作。
A生产车间(){
    while(1){
        生产零件A;
        P(货架F1有空位?若有,空位-1,若无则阻塞); //
        把零件A放到货架F1上;
    }
}

B生产车间(){
    while(1){
        生产零件B;
        把零件B放到货架F2上;
    }
}

装配车间(){
    while(1){
        从货架F1上取出零件A;
        V(货架F1空位+1); //
        从货架F2上取出零件B;
        零件A和零件B组装产品;
    }
}

(2)B 生产车间进程

  • 生产零件B之前不需要 P 操作。
  • 把零件B放到货架F2上之前,需要先检查货架 F2 是否有空位,如果有空位,那么空位 - 1,表明该零件占据一个空位;否则进程堵塞。因此需要一个 P 操作。
    • 记住,添加一个 P 操作就需要配对相应的 V 操作。跟之前的操作相反,什么地方需要空位 + 1?在装配车间进程的从货架F2上取出零件B之后,零件已被取出,空位 + 1,因此需要一个 V 操作。
A生产车间(){
    while(1){
        生产零件A;
        P(货架F1有空位?若有,空位-1,若无则阻塞);
        把零件A放到货架F1上;
    }
}

B生产车间(){
    while(1){
        生产零件B;
        P(货架F2有空位?若有,空位-1,若无则阻塞); //
        把零件B放到货架F2上;
    }
}

装配车间(){
    while(1){
        从货架F1上取出零件A;
        V(货架F1空位+1);
        从货架F2上取出零件B;
        V(货架F2空位+1); //
        零件A和零件B组装产品;
    }
}

(3)装配车间进程

  • 从货架F1上取出零件A之前,需要先检查货架 F1 是否有零件 A,如果有零件 A,那么零件 A - 1,表明该进程已经取走了一个零件 A;否则进程堵塞。因此需要一个 P 操作。
    • 记住,添加一个 P 操作就需要配对相应的 V 操作。跟之前的操作相反,什么地方需要零件 A + 1?在 A 生产车间进程的把零件A放到货架F1上之后,零件 A + 1,因此需要一个 V 操作。
  • 从货架F2上取出零件B之前,需要先检查货架 F2 是否有零件 B,如果有零件 B,那么零件 B - 1,表明该进程已经取走了一个零件 B;否则进程堵塞。因此需要一个 P 操作。
    • 记住,添加一个 P 操作就需要配对相应的 V 操作。跟之前的操作相反,什么地方需要零件 B + 1?在 B 生产车间进程的把零件B放到货架F2上之后,零件 B + 1,因此需要一个 V 操作。
  • 零件A和零件B组装产品之前不需要 P 操作。
A生产车间(){
    while(1){
        生产零件A;
        P(货架F1有空位?若有,空位-1,若无则阻塞);
        把零件A放到货架F1上;
        V(货架F1的零件A+1); //
    }
}

B生产车间(){
    while(1){
        生产零件B;
        P(货架F2有空位?若有,空位-1,若无则阻塞);
        把零件B放到货架F2上;
        V(货架F2的零件B+1); //
    }
}

装配车间(){
    while(1){
        P(货架F1上是否有零件A?若有,零件A-1,若无则阻塞);
        从货架F1上取出零件A;
        V(货架F1空位+1); //
        P(货架F2上是否有零件B?若有,零件B-1,若无则阻塞);
        从货架F2上取出零件B;
        V(货架F2空位+1); //
        零件A和零件B组装产品;
    }
}

(4)货架 F1 和 F2

由于货架 F1 和货架 F2 不能同时进行读写,所以各需要一个互斥锁,夹紧访问缓冲区的操作。

A生产车间(){
    while(1){
        生产零件A;
        P(货架F1有空位?若有,空位-1,若无则阻塞);
        P(F1互斥锁); //
        把零件A放到货架F1上;
        V(F1互斥锁); //
        V(货架F1的零件A+1);
    }
}

B生产车间(){
    while(1){
        生产零件B;
        P(货架F2有空位?若有,空位-1,若无则阻塞);
        P(F2互斥锁); //
        把零件B放到货架F2上;
        V(F2互斥锁); //
        V(货架F2的零件B+1);
    }
}

装配车间(){
    while(1){
        P(货架F1上是否有零件A?若有,零件A-1,若无则阻塞);
        P(F1互斥锁); //
        从货架F1上取出零件A;
        V(F1互斥锁); //
        V(货架F1空位+1);
        P(货架F2上是否有零件B?若有,零件B-1,若无则阻塞);
        P(F2互斥锁); //
        从货架F2上取出零件B;
        V(F2互斥锁); //
        V(货架F2空位+1);
        零件A和零件B组装产品;
    }
}

Step 4. 检查是否出现死锁

没有连续的 P 操作,因此不会发生死锁。

Step 5. 定义信号量

把所有 PV 操作的中文全部换成英文即可。由于我们写的是中文,所以需要注意 PV 操作对应的信号量是什么,以及是否相同。

信号量的初始值一般为题目中给出的初始值。互斥信号量的初始值一般为 1。

  • 货架 F1 空位信号量:初始值应为 10。
  • 货架 F2 空位信号量:初始值应为 10。
  • 零件 A信号量:初始值应为 0。
  • 零件 B信号量:初始值应为 0。
  • 货架 F1 互斥信号量:初始值应为 1。
  • 货架 F2 互斥信号量:初始值应为 1。
信号量 emptyF1 = 10;
信号量 emptyF2 = 10;
信号量 fullA = 0;
信号量 fullB = 0;
信号量 mutexF1 = 1;
信号量 mutexF2 = 1;

A生产车间(){
    while(1){
        生产零件A;
        P(emptyF1);
        P(mutexF1);
        把零件A放到货架F1上;
        V(mutexF1);
        V(fullA);
    }
}

B生产车间(){
    while(1){
        生产零件B;
        P(emptyF2);
        P(mutexF2);
        把零件B放到货架F2上;
        V(mutexF2);
        V(fullB);
    }
}

装配车间(){
    while(1){
        P(fullA);
        P(mutexF1);
        从货架F1上取出零件A;
        V(mutexF1);
        V(emptyF1);
        P(fullB);
        P(mutexF2);
        从货架F2上取出零件B;
        V(mutexF2);
        V(emptyF2);
        零件A和零件B组装产品;
    }
}

题目 4:单消费者、单生产者、单缓冲区、连续取出

【题目 4】系统中有多个生产者进程和多个消费者进程,共享一个能存放 1000 件产品的环形缓冲区(初始为空)。当缓冲区未满时,生产者进程可以放入其生产的一件产品,否则等待;当缓冲区未空时,消费者进程可以从缓冲区取走一件产品,否则等待。要求:一个消费者进程从缓冲区连续取出 10 件产品后,其他消费者进程才可以取产品。请使用信号量 P,V(wait(),signal())操作实现进程间的互斥与同步,要求写出完整的过程,并说明所用信号量的含义和初值。

Step 1. 有几类进程

两类:生产者、消费者。

两类进程都是不断重复执行,因而需要加上循环体。

写出代码框架:

生产者(){
    while(1){
        
    }
}

消费者(){
    while(1){
    
    }
}

Step 2. 中文描述动作

生产者(){
    while(1){
        生产产品;
        把产品放入缓冲区;
    }
}

消费者(){
    while(1){
        for(i = 0; i < 10; i++){
            从缓冲区取出产品;
        }
        消费产品;
    }
}

Step 3. 添加 PV 操作,用中文描述里面的操作

(1)生产者进程

  • 生产产品之前不需要 P 操作。
  • 把产品放入缓冲区之前,需要先检查缓冲区是否有空位,如果有空位,那么空位 - 1,表明该产品占据一个空位;否则进程堵塞。因此需要一个 P 操作。
    • 记住,添加一个 P 操作就需要配对相应的 V 操作。跟之前的操作相反,什么地方需要空位 + 1?在消费者进程的从缓冲区取出产品之后,产品已被取出,空位 + 1,因此需要一个 V 操作。
生产者(){
    while(1){
        生产产品;
        P(缓冲区有空位?若有,空位-1,若无则阻塞); //
        把产品放入缓冲区;
    }
}

消费者(){
    while(1){
        for(i = 0; i < 10; i++){
            从缓冲区取出产品;
            V(空位+1); //
        }
        消费产品;
    }
}

(2)消费者进程

  • 从缓冲区取出产品之前,需要先检查缓冲区是否有产品,如果有产品,那么产品 - 1,表明该进程已经取走了一个产品;否则进程堵塞。因此需要一个 P 操作。
    • 记住,添加一个 P 操作就需要配对相应的 V 操作。跟之前的操作相反,什么地方需要产品 + 1?在生产者进程的把产品放入缓冲区之后,产品 + 1,因此需要一个 V 操作。
  • 消费产品之前不需要 P 操作。
生产者(){
    while(1){
        生产产品;
        P(缓冲区有空位?若有,空位-1,若无则阻塞);
        把产品放入缓冲区;
        V(产品+1); //
    }
}

消费者(){
    while(1){
        for(i = 0; i < 10; i++){
            P(缓冲区有产品?若有,产品-1,若无则阻塞); //
            从缓冲区取出产品;
            V(空位+1);
        }
        消费产品;
    }
}

(3)缓冲区

由于缓冲区不能同时进行读写,所以需要一个互斥锁,夹紧访问缓冲区的操作。

生产者(){
    while(1){
        生产产品;
        P(缓冲区有空位?若有,空位-1,若无则阻塞);
        P(互斥锁); //
        把产品放入缓冲区;
        V(互斥锁); //
        V(产品+1);
    }
}

消费者(){
    while(1){
        for(i = 0; i < 10; i++){
            P(缓冲区有产品?若有,产品-1,若无则阻塞);
            P(互斥锁); //
            从缓冲区取出产品;
            V(互斥锁); //
            V(空位+1);
        }
        消费产品;
    }
}

Step 4. 检查是否出现死锁

发现消费者进程出现了连续的 10 次 P 操作,可能会发生死锁。因此,在取产品之前和取完产品之后,需要加上另一对大的互斥锁,把整个取出过程给套住。

生产者(){
    while(1){
        生产产品;
        P(缓冲区有空位?若有,空位-1,若无则阻塞);
        P(互斥锁); 
        把产品放入缓冲区;
        V(互斥锁); 
        V(产品+1);
    }
}

消费者(){
    while(1){
        P(大互斥锁); //
        for(i = 0; i < 10; i++){
            P(缓冲区有产品?若有,产品-1,若无则阻塞);
            P(互斥锁); 
            从缓冲区取出产品;
            V(互斥锁); 
            V(空位+1);
        }
        V(大互斥锁); //
        消费产品;
    }
}

Step 5. 定义信号量

把所有 PV 操作的中文全部换成英文即可。由于我们写的是中文,所以需要注意 PV 操作对应的信号量是什么,以及是否相同。

信号量的初始值一般为题目中给出的初始值。互斥信号量的初始值一般为 1。

  • 空位信号量:初始值应为 1000。
  • 产品信号量:初始值应为 0。
  • 互斥信号量:初始值应为 1。
  • 大互斥信号量:初始值应为 1。
信号量 empty = 1000;
信号量 full = 0;
信号量 mutex = 1;
信号量 mutex1 = 1;

生产者(){
    while(1){
        生产产品;
        P(empty);
        P(mutex);
        把产品放入缓冲区;
        V(mutex);
        V(full);
    }
}

消费者(){
    while(1){
        P(mutex1); 
        for(i = 0; i < 10; i++){
            P(full);
            P(mutex);
            从缓冲区取出产品;
            V(mutex);
            V(empty);
        }
        V(mutex1); 
        消费产品;
    }
}

题目 5:既是生产者又是消费者、双缓冲区、单次取出

【题目 5】(信箱辩论问题)有 A、B 两人通过信箱进行辩论,每个人都从自己的信箱中取得对方的问题。将答案和向对方提出的新问题组成一个邮件放入对方的邮箱中。假设 A 的信箱最多放 M 个邮件,B 的信箱最多 放 N 个邮件。初始时 A 的信箱中有 x 个邮件(0 < x < M),B 的信箱中有 y 个(0 < y < N)。辩论者每取出 一个邮件,邮件数减 1。A 和 B 两人的操作过程描述如下:

CoBegin
 
A{
    while(TRUE){
        从A的信箱中取出一个邮件; 
        回答问题并提出一个新问题; 
        将新邮件放入B的信箱;
    }
}
 
B{
    while(TRUE){
        从B的信箱中取出一个邮件; 
        回答问题并提出一个新问题; 
        将新邮件放入A的信箱;
    }
}

CoEnd

当信箱不为空时,辩论者才能从信箱中取邮件,否则等待。当信箱不满时,辩论者才能将新邮件放入信箱,否则等待。请添加必要的信号量和P、V(或 wait、signal)操作,以实现上述过程的同步。 要求写出完整过程,并说明信号量的含义和初值。

Step 1. 有几类进程

题目已给出。

Step 2. 用中文描述动作

题目已给出。

Step 3. 添加 PV 操作,用中文描述里面的操作

(1)A 进程

  • 从A的信箱中取出一个邮件之前,需要先检查 A 箱是否有邮件,如果有邮件,那么邮件 - 1,表明该进程已经取走了一个邮件;否则进程堵塞。因此需要一个 P 操作。
    • 记住,添加一个 P 操作就需要配对相应的 V 操作。跟之前的操作相反,什么地方需要邮件 + 1?在 B 进程的将新邮件放入A的信箱之后,邮件 + 1,因此需要一个 V 操作。
  • 回答问题并提出一个新问题之前不需要 P 操作。
  • 将新邮件放入B的信箱之前,需要先检查 B 箱中是否有空位,如果有空位,那么空位 - 1,表明该邮件占据一个空位;否则进程堵塞。因此需要一个 P 操作。
    • 记住,添加一个 P 操作就需要配对相应的 V 操作。跟之前的操作相反,什么地方需要空位 + 1?在 B 进程的从B的信箱中取出一个邮件之后,邮件已被取出,空位 + 1,因此需要一个 V 操作。
CoBegin
 
A{
    while(TRUE){
        P(A箱中有邮件?若有,邮件-1,若无则阻塞); //
        从A的信箱中取出一个邮件; 
        回答问题并提出一个新问题; 
        P(B箱中有空位?若有,空位-1,若无则阻塞); //
        将新邮件放入B的信箱;
    }
}
 
B{
    while(TRUE){
        从B的信箱中取出一个邮件;
        V(B箱空位+1); //
        回答问题并提出一个新问题; 
        将新邮件放入A的信箱;
        V(A箱邮件+1); //
    }
}

CoEnd

(2)B 进程

  • 从B的信箱中取出一个邮件之前,需要先检查 B 箱是否有邮件,如果有邮件,那么邮件 - 1,表明该进程已经取走了一个邮件;否则进程堵塞。因此需要一个 P 操作。
    • 记住,添加一个 P 操作就需要配对相应的 V 操作。跟之前的操作相反,什么地方需要邮件 + 1?在 A 进程的将新邮件放入B的信箱之后,邮件 + 1,因此需要一个 V 操作。
  • 回答问题并提出一个新问题之前不需要 P 操作。
  • 将新邮件放入A的信箱之前,需要先检查 A 箱中是否有空位,如果有空位,那么空位 - 1,表明该邮件占据一个空位;否则进程堵塞。因此需要一个 P 操作。
    • 记住,添加一个 P 操作就需要配对相应的 V 操作。跟之前的操作相反,什么地方需要空位 + 1?在 A 进程的从A的信箱中取出一个邮件之后,邮件已被取出,空位 + 1,因此需要一个 V 操作。
CoBegin
 
A{
    while(TRUE){
        P(A箱中有邮件?若有,邮件-1,若无则阻塞);
        从A的信箱中取出一个邮件; 
        V(A箱空位+1); //
        回答问题并提出一个新问题; 
        P(B箱中有空位?若有,空位-1,若无则阻塞);
        将新邮件放入B的信箱;
        V(B箱邮件+1); //
    }
}
 
B{
    while(TRUE){
        P(B箱中有邮件?若有,邮件-1,若无则阻塞); //
        从B的信箱中取出一个邮件;
        V(B箱空位+1);
        回答问题并提出一个新问题; 
        P(A箱中有空位?若有,空位-1,若无则阻塞); //
        将新邮件放入A的信箱;
        V(A箱邮件+1);
    }
}

CoEnd

(3)缓冲区

由于 A 箱、B 箱均不能同时进行读写,所以各需要一个互斥锁,夹紧访问缓冲区的操作。

CoBegin
 
A{
    while(TRUE){
        P(A箱中有邮件?若有,邮件-1,若无则阻塞);
        P(A箱互斥锁); //
        从A的信箱中取出一个邮件; 
        V(A箱互斥锁); //
        V(A箱空位+1);
        回答问题并提出一个新问题; 
        P(B箱中有空位?若有,空位-1,若无则阻塞);
        P(B箱互斥锁); //
        将新邮件放入B的信箱;
        V(B箱互斥锁); //
        V(B箱邮件+1);
    }
}
 
B{
    while(TRUE){
        P(B箱中有邮件?若有,邮件-1,若无则阻塞);
        P(B箱互斥锁); //
        从B的信箱中取出一个邮件;
        V(B箱互斥锁); //
        V(B箱空位+1);
        回答问题并提出一个新问题; 
        P(A箱中有空位?若有,空位-1,若无则阻塞);
        P(A箱互斥锁); //
        将新邮件放入A的信箱;
        V(A箱互斥锁); //
        V(A箱邮件+1);
    }
}

CoEnd

Step 4. 检查是否出现死锁

没有连续的 P 操作,因此不会发生死锁,上面即为本题答案。

Step 5. 定义信号量

把所有 PV 操作的中文全部换成英文即可。由于我们写的是中文,所以需要注意 PV 操作对应的信号量是什么,以及是否相同。

信号量的初始值一般为题目中给出的初始值。互斥信号量的初始值一般为 1。

注意,初始时 A 的信箱中有 x 个邮件(0 < x < M),B 的信箱中有 y 个(0 < y < N)。因此,空位的初始值分别应为 M-x 和 N-y。

  • A箱空位信号量:初始值应为 M-x。
  • B箱空位信号量:初始值应为 N-y。
  • A箱邮件信号量:初始值应为 x。
  • B箱邮件信号量:初始值应为 y。
  • A箱互斥信号量:初始值应为 1。
  • B箱互斥信号量:初始值应为 1。
CoBegin

信号量 emptyA = M-x;
信号量 emptyB = N-y;
信号量 fullA  = x;
信号量 fullB  = y;
信号量 mutexA = 1;
信号量 mutexB = 1;
 
A{
    while(TRUE){
        P(fullA);
        P(mutexA);
        从A的信箱中取出一个邮件; 
        V(mutexA);
        V(emptyA);
        回答问题并提出一个新问题; 
        P(emptyB);
        P(mutexB);
        将新邮件放入B的信箱;
        V(mutexB);
        V(fullB);
    }
}
 
B{
    while(TRUE){
        P(fullB);
        P(mutexB);
        从B的信箱中取出一个邮件;
        V(mutexB);
        V(emptyB);
        回答问题并提出一个新问题; 
        P(emptyA);
        P(mutexA);
        将新邮件放入A的信箱;
        V(mutexA);
        V(fullA);
    }
}

CoEnd

题目 6:双生产者、单消费者、单缓冲区、连续取出

【题目 6】设自行车生产线上有一个箱子,其中有 N 个位置(N ≥ 3),每个位置可存放一个车架或一个车轮,又设有 3 名工人,其活动分别为:

工人 1(){
    while(1){
        加工一个车架;
        车架放入箱中;
    }
}

工人 2(){
    while(1){
        加工一个车轮;
        车轮放入箱中;
    }
}

工人 3(){
    while(1){
        箱中取出一个车架;
        箱中取出二个车轮;
        组装为一台车;
    }
}

试分别用信号量与 PV 操作实现三名工人的合作,要求解中不含死锁。

Step 1. 有几类进程

题目已给出。

Step 2. 用中文描述动作

题目已给出。

Step 3. 添加 PV 操作,用中文描述里面的操作

(1)工人 1 进程

  • 加工一个车架之前不需要 P 操作。
  • 车架放入箱中之前,需要先检查箱中是否有空位,如果有空位,那么空位 - 1,表明该产品占据一个空位;否则进程堵塞。因此需要一个 P 操作。
    • 记住,添加一个 P 操作就需要配对相应的 V 操作。跟之前的操作相反,什么地方需要空位 + 1?在工人 3 进程的箱中取出一个车架之后,产品已被取出,空位 + 1,因此需要一个 V 操作。
工人 1(){
    while(1){
        加工一个车架;
        P(箱中有空位?若有,空位-1,若无则阻塞); //
        车架放入箱中;
    }
}

工人 2(){
    while(1){
        加工一个车轮;
        车轮放入箱中;
    }
}

工人 3(){
    while(1){
        箱中取出一个车架;
        V(箱的空位+1); //
        箱中取出二个车轮;
        组装为一台车;
    }
}

(2)工人 2 进程

  • 加工一个车轮之前不需要 P 操作。
  • 车轮放入箱中之前,需要先检查箱中是否有空位,如果有空位,那么空位 - 1,表明该产品占据一个空位;否则进程堵塞。因此需要一个 P 操作。
    • 记住,添加一个 P 操作就需要配对相应的 V 操作。跟之前的操作相反,什么地方需要空位 + 1?在工人 3 进程的箱中取出二个车轮之后,产品已被取出,空位 + 1,注意了,进程连续两次取出车轮,因此需要两个 V 操作。
工人 1(){
    while(1){
        加工一个车架;
        P(箱中有空位?若有,空位-1,若无则阻塞); 
        车架放入箱中;
    }
}

工人 2(){
    while(1){
        加工一个车轮;
        P(箱中有空位?若有,空位-1,若无则阻塞); //
        车轮放入箱中;
    }
}

工人 3(){
    while(1){
        箱中取出一个车架;
        V(箱的空位+1);
        箱中取出二个车轮;
        V(箱的空位+1); //
        V(箱的空位+1); //
        组装为一台车;
    }
}

(3)工人 3 进程

  • 箱中取出一个车架之前,需要先检查箱中是否有车架,如果有车架,那么车架 - 1,表明该进程已取走一个车架;否则进程堵塞。因此需要一个 P 操作。
    • 记住,添加一个 P 操作就需要配对相应的 V 操作。跟之前的操作相反,什么地方需要车架 + 1?在工人 1 进程的车架放入箱中之后,车架 + 1,因此需要 V 操作。
  • 箱中取出二个车轮之前,需要先检查箱中是否有两个车轮,如果有两个车轮,那么车架 - 2,表明该进程已取走两个车架;否则进程堵塞。因此需要两个 P 操作。
    • 记住,添加一个 P 操作就需要配对相应的 V 操作。跟之前的操作相反,什么地方需要车架 + 1?在工人 2 进程的车轮放入箱中之后,车轮 + 1,因此需要 V 操作。
  • 组装为一台车之前不需要 P 操作。
工人 1(){
    while(1){
        加工一个车架;
        P(箱中有空位?若有,空位-1,若无则阻塞);
        车架放入箱中;
        V(车架+1);
    }
}

工人 2(){
    while(1){
        加工一个车轮;
        P(箱中有空位?若有,空位-1,若无则阻塞);
        车轮放入箱中;
        V(车轮+1); 
    }
}

工人 3(){
    while(1){
        P(箱中有车架?若有,车架-1,若无则阻塞); 
        箱中取出一个车架;
        V(箱的空位+1);
        
        P(箱中有车轮?若有,车轮-1,若无则阻塞); 
        P(箱中有车轮?若有,车轮-1,若无则阻塞); 
        箱中取出二个车轮;
        V(箱的空位+1);
        V(箱的空位+1);
        
        组装为一台车;
    }
}

(4)缓冲区

由于箱均不能同时进行读写,所以需要一个互斥锁,夹紧访问缓冲区的操作。

工人 1(){
    while(1){
        加工一个车架;
        P(箱中有空位?若有,空位-1,若无则阻塞);
        P(互斥锁);
        车架放入箱中;
        V(互斥锁);
        V(车架+1);
    }
}

工人 2(){
    while(1){
        加工一个车轮;
        P(箱中有空位?若有,空位-1,若无则阻塞);
        P(互斥锁);
        车轮放入箱中;
        V(互斥锁);
        V(车轮+1); 
    }
}

工人 3(){
    while(1){
        P(箱中有车架?若有,车架-1,若无则阻塞); 
        P(互斥锁);
        箱中取出一个车架;
        V(互斥锁);
        V(箱的空位+1);
        
        P(箱中有车轮?若有,车轮-1,若无则阻塞); 
        P(箱中有车轮?若有,车轮-1,若无则阻塞); 
        P(互斥锁);
        箱中取出二个车轮;
        V(互斥锁);
        V(箱的空位+1);
        V(箱的空位+1);
        
        组装为一台车;
    }
}

Step 4. 检查是否出现死锁

注意,题目 3 是两个生产者进程各自使用自己的缓冲区;而这道题是两个生产者进程共用一个缓冲区,此时就必须要考虑两个进程谁快谁慢的问题了,因为如果其中一个进程太快,就会导致缓冲区很快被其中一种产品全部占满,从而发生死锁现象。现在就来考虑下面几种情形:

  • 如果工人 1 速度很快,箱子很快就被车架占满了,工人 2 没法放车轮,工人 3 没法造车,产生死锁;
  • 如果工人 2 速度很快,箱子很快就被车轮占满了,工人 1 没法放车轮,工人 3 没法造车,产生死锁;
  • 如果工人 1 速度很快,箱子很快就被车架占用了,只剩下一个空位,工人 2 放一个车轮,工人 3 只能获取一个车架和一个车轮,没法造车,产生死锁。

解决办法:

  • 对于工人 1,当他发现空位数只剩下两个的时候(即空位 = N-2),他就不能再放车架了,必须要让工人 2 放车轮;
  • 对于工人 2,当他发现空位数只剩下一个的时候(即空位 = N-1),他就不能再放车轮了,必须要让工人 1 放车架。

也就是说,实际上车架最多只能放 N-1 个,车轮最多只能放 N-2 个。所以此时需要再构造两个虚拟的缓冲区,把共享缓冲区问题转化为双缓冲区问题:

  • 车架缓冲区,初始空位有 N-1 个;
  • 车轮缓冲区,初始空位有 N-2 个。

因此需要再添加 PV 操作:

工人 1(){
    while(1){
        加工一个车架;
        P(箱中有空位?若有,空位-1,若无则阻塞);
        P(车架缓冲区是否有空位?若是,空位-1,若无则阻塞);  //
        P(互斥锁);
        车架放入箱中;
        V(互斥锁);
        V(车架+1);
    }
}

工人 2(){
    while(1){
        加工一个车轮;
        P(箱中有空位?若有,空位-1,若无则阻塞);
        P(车轮缓冲区是否有空位?若是,空位-1,若无则阻塞); //
        P(互斥锁);
        车轮放入箱中;
        V(互斥锁);
        V(车轮+1); 
    }
}

工人 3(){
    while(1){
        P(箱中有车架?若有,车架-1,若无则阻塞); 
        P(互斥锁);
        箱中取出一个车架;
        V(互斥锁);
        V(箱的空位+1);
        V(车架缓冲区的空位+1); //
        
        P(箱中有车轮?若有,车轮-1,若无则阻塞); 
        P(箱中有车轮?若有,车轮-1,若无则阻塞); 
        P(互斥锁);
        箱中取出二个车轮;
        V(互斥锁);
        V(箱的空位+1);
        V(箱的空位+1);
        V(车轮缓冲区的空位+1); //
        V(车轮缓冲区的空位+1); //
        
        组装为一台车;
    }
}

Step 5. 定义信号量

把所有 PV 操作的中文全部换成英文即可。由于我们写的是中文,所以需要注意 PV 操作对应的信号量是什么,以及是否相同。

信号量的初始值一般为题目中给出的初始值。互斥信号量的初始值一般为 1。

  • 箱空位信号量:初始值应为 N。
  • 箱中车架信号量:初始值应为 0。
  • 箱中车轮信号量:初始值应为 0。
  • 箱互斥信号量:初始值应为 1。
  • 车架缓冲区空位信号量:初始值应为 N-1。
  • 车轮缓冲区空位信号量:初始值应为 N-2。
信号量 empty = N;
信号量 chejia = 0;
信号量 chelun = 0;
信号量 mutex = 1;
信号量 chejia_empty = N-1; 
信号量 chelun_empty = N-2;

工人 1(){
    while(1){
        加工一个车架;
        P(empty);
        P(chejia_empty);
        P(mutex);
        车架放入箱中;
        V(mutex);
        V(chejia);
    }
}

工人 2(){
    while(1){
        加工一个车轮;
        P(empty);
        P(chelun_empty);
        P(mutex);
        车轮放入箱中;
        V(mutex);
        V(chelun); 
    }
}

工人 3(){
    while(1){
        P(chejia); 
        P(mutex);
        箱中取出一个车架;
        V(mutex);
        V(empty);
        V(chejia_empty);
        
        P(chelun); 
        P(chelun); 
        P(mutex);
        箱中取出二个车轮;
        V(mutex);
        V(empty);
        V(empty);
        V(chelun_empty);
        V(chelun_empty);
        
        组装为一台车;
    }
}

题目 7:单生产者、多消费者、单缓冲区、约束取出

【题目 7】三个进程 P1、P2、P3 互斥使用一个包含 N(N>0)个单元的缓冲区。

  • P1 每次用 produce() 生成一个正整数并用 put() 送入缓冲区某一空单元中;
  • P2 每次用 getodd() 从该缓冲区中取出一个奇数并用 countodd() 统计奇数个数;
  • P3 每次用 geteven() 从该缓冲区中取出一个偶数并用 counteven() 统计偶数个数。

请用信号量机制实现这三个进程的同步与互斥活动,并说明所定义的信号量的含义。要求用伪代码描述。

Step 1. 有几类进程

三类:P1、P2、P3。

三类进程都是不断重复执行,因而需要加上循环体。

写出代码框架:

P1(){
    while(1){
        
    }
}

P2(){
    while(1){
    
    }
}

P3(){
    while(1){
        
    }
}

Step 2. 中文描述动作

P1(){
    while(1){
        produce();
        put();
    }
}

P2(){
    while(1){
        getodd();
        countodd();
    }
}

P3(){
    while(1){
        geteven();
        counteven();
    }
}

按题意,进一步完善操作:

P1(){
    while(1){
        n = produce();
        put(n);
        如果 n 是奇数{
            通知 P2 开始工作;
        }
        否则 n 是偶数{
            通知 P3 开始工作;
        }
    }
}

P2(){
    while(1){
        getodd();
        countodd();
    }
}

P3(){
    while(1){
        geteven();
        counteven();
    }
}

Step 3. 添加 PV 操作,用中文描述里面的操作

(1)P1 进程

  • n = produce()之前不需要 P 操作。
  • put(n)之前,需要先检查缓冲区是否有空位,如果有空位,那么空位 - 1,表明该产品占据一个空位;否则进程堵塞。因此需要一个 P 操作。
    • 记住,添加一个 P 操作就需要配对相应的 V 操作。跟之前的操作相反,什么地方需要空位 + 1?在 P2 和 P3 进程的getodd()geteven()之后,产品已被取出,空位 + 1,因此需要一个 V 操作。
  • 如果读者有印象并且能看得出的话,通知 P2 开始工作通知 P3 开始工作本质上是一个前驱后继的信号量实现,只有 P1 运行完,P2 和 P3 才能开始运行。前驱进程运行完后,需要 V 操作;后继进程运行前,需要 P 操作。
P1(){
    while(1){
        n = produce();
        P(缓冲区是否有空位?若是,空位-1,若无则阻塞); //
        put(n);
        如果 n 是奇数{
            V(通知 P2 开始工作); //
        }
        否则 n 是偶数{
            V(通知 P3 开始工作); //
        }
    }
}

P2(){
    while(1){
        P(P2 开始工作); //
        getodd();
        V(空位+1); //
        countodd();
    }
}

P3(){
    while(1){
        P(P3 开始工作); //
        geteven();
        V(空位+1); //
        counteven();
    }
}

(2)P2 和 P3 进程

  • 在 P2 进程的getodd()和在 P3 进程的geteven()之前,不需要先检查缓冲区是否有数字,因为这是前驱后继的关系,P1 进程通知 P2、P3 进程工作,缓冲区里就一定会有数字。
  • countdd()counteven()之前不需要 P 操作。
P1(){
    while(1){
        n = produce();
        P(缓冲区是否有空位?若是,空位-1,若无则阻塞);
        put(n);
        如果 n 是奇数{
            V(通知 P2 开始工作);
        }
        否则 n 是偶数{
            V(通知 P3 开始工作);
        }
    }
}

P2(){
    while(1){
        P(P2 开始工作);
        getodd();
        V(空位+1);
        countodd();
    }
}

P3(){
    while(1){
        P(P3 开始工作);
        geteven();
        V(空位+1);
        counteven();
    }
}

(3)缓冲区

由于缓冲区不能同时进行读写,所以需要一个互斥锁,夹紧访问缓冲区的操作。

P1(){
    while(1){
        n = produce();
        P(缓冲区是否有空位?若是,空位-1,若无则阻塞);
        P(互斥锁); //
        put(n);
        V(互斥锁); //
        如果 n 是奇数{
            V(通知 P2 开始工作);
        }
        否则 n 是偶数{
            V(通知 P3 开始工作);
        }
    }
}

P2(){
    while(1){
        P(P2 开始工作);
        P(互斥锁); //
        getodd();
        V(互斥锁); //
        V(空位+1);
        countodd();
    }
}

P3(){
    while(1){
        P(P3 开始工作);
        P(互斥锁); //
        geteven();
        V(互斥锁); //
        V(空位+1);
        counteven();
    }
}

Step 4. 检查是否出现死锁

不会发生死锁。

Step 5. 定义信号量

把所有 PV 操作的中文全部换成英文即可。由于我们写的是中文,所以需要注意 PV 操作对应的信号量是什么,以及是否相同。

信号量的初始值一般为题目中给出的初始值。互斥信号量的初始值一般为 1。

前驱后继的信号量的初始值一般为 0。

  • 空位信号量:初始值应为 N。
  • P2工作信号量:初始值应为 0。
  • P3工作信号量:初始值应为 0。
  • 互斥信号量:初始值应为 1。
信号量 empty = N;
信号量 P2 = 0;
信号量 P3 = 0;
信号量 mutex = 1;

P1(){
    while(1){
        n = produce();
        
        P(empty);
        P(mutex);
        put(n);
        V(mutex);
        
        if (n % 2 != 0){
            V(P2);
        }
        else{
            V(P3);
        }
    }
}

P2(){
    while(1){
        P(P2);
        
        P(mutex);
        getodd();
        V(mutex);
        V(empty);
        
        countodd();
    }
}

P3(){
    while(1){
        P(P3);
        
        P(mutex);
        geteven();
        V(mutex);
        V(empty);
        
        counteven();
    }
}

题目 8:单生产者、单消费者、多类资源需求

【题目 8】某寺庙有小和尚和老和尚若干,有一个水缸,由小和尚提水入缸供老和尚饮用。水缸可以容纳 10 桶水,水取自同一口井中,由于水井口窄,每次只能容纳一个水桶取水。水桶总数为3个。每次入水、取水仅为一桶,且不可同时进行。试给出有关取水、入水的算法描述。

Step 1. 有几类进程

两类:小和尚、老和尚。

三类进程都是不断重复执行,因而需要加上循环体。

小和尚(){
    while(1){
        
    }
}

老和尚(){
    while(1){
        
    }
}

Step 2. 中文描述动作

小和尚(){
    while(1){
        取桶;
        到水井打水;
        往缸里倒水;
    }
}

老和尚(){
    while(1){
        取桶;
        到水缸打水;
    }
}

Step 3. 添加 PV 操作,用中文描述里面的操作

(1)小和尚进程

  • 取桶之前,需要先检查是否有桶,如果有桶,那么桶 - 1,表明该小和尚取走了桶;否则进程堵塞。因此需要一个 P 操作。
    • 记住,添加一个 P 操作就需要配对相应的 V 操作。跟之前的操作相反,什么地方需要桶 + 1?在小和尚进程的往缸里倒水之后,桶已经不用了,归还桶,桶 + 1,因此需要一个 V 操作。
    • 还有,在老和尚进程的到水缸打水之后,桶已经不用了,归还桶,桶 + 1,因此也需要一个 V 操作。
  • 到水井打水之前,需要先检查井是否有空位,如果有空位,那么井空位 - 1,表明该小和尚准备井里取水;否则进程堵塞。因此需要一个 P 操作。
    • 记住,添加一个 P 操作就需要配对相应的 V 操作。跟之前的操作相反,什么地方需要井空位 + 1?在小和尚进程的到水井打水之后,井空位 + 1,因此需要一个 V 操作。
  • 往缸里倒水之前,需要先检查水缸是否有空位,如果有空位,那么空位 - 1,表明水占用水缸的空位;否则进程堵塞。因此需要一个 P 操作。
    • 记住,添加一个 P 操作就需要配对相应的 V 操作。跟之前的操作相反,什么地方需要空位 + 1?在老和尚进程的到水缸打水之后,水缸空位 + 1,因此需要一个 V 操作。
小和尚(){
    while(1){
        P(是否有桶?若有,桶-1,否则阻塞等待); //
        取桶;
        P(水井是否有空位?若有,空位-1,否则阻塞等待); //
        到水井打水;
        V(水井空位+1); //
        P(水缸是否有空位?若有,空位-1,否则阻塞等待); //
        往缸里倒水;
        V(桶+1); //
    }
}

老和尚(){
    while(1){
        取桶;
        到水缸打水;
        V(桶+1); //
        V(水缸空位+1); //
    }
}

(2)老和尚进程

  • 取桶之前,需要先检查是否有桶,如果有桶,那么桶 - 1,表明该小和尚取走了桶;否则进程堵塞。因此需要一个 P 操作。
    • 记住,添加一个 P 操作就需要配对相应的 V 操作。跟之前的操作相反,什么地方需要桶 + 1?在小和尚进程的往缸里倒水之后,桶已经不用了,归还桶,桶 + 1,因此需要一个 V 操作。(已经加过了)
    • 还有,在老和尚进程的到水缸打水之后,桶已经不用了,归还桶,桶 + 1,因此也需要一个 V 操作。(已经加过了)
  • 到水缸打水之前,需要先检查水缸是否有水,如果有水,那么水 - 1,表明该老和尚准备在水缸取水;否则进程堵塞。因此需要一个 P 操作。
    • 记住,添加一个 P 操作就需要配对相应的 V 操作。跟之前的操作相反,什么地方需要水 + 1?在小和尚进程的往缸里倒水之后,水 + 1,因此需要一个 V 操作。
小和尚(){
    while(1){
        P(是否有桶?若有,桶-1,否则阻塞等待);
        取桶;
        P(水井是否有空位?若有,空位-1,否则阻塞等待);
        到水井打水;
        V(水井空位+1);
        P(水缸是否有空位?若有,空位-1,否则阻塞等待);
        往缸里倒水;
        V(桶+1); //
        V(水+1); //
    }
}

老和尚(){
    while(1){
        P(是否有桶?若有,桶-1,否则阻塞等待); //
        取桶;
        P(水缸是否有水?若有,水-1,否则阻塞等待); //
        到水缸打水;
        V(桶+1); //
        V(水缸空位+1); 
    }
}

(3)缓冲区

由于水缸、水井和水桶不能同时进行使用,所以分别需要三个互斥锁,夹紧访问临界资源的操作。

小和尚(){
    while(1){
        P(是否有桶?若有,桶-1,否则阻塞等待);
        P(互斥锁-桶);
        取桶;
        V(互斥锁-桶);
        
        P(水井是否有空位?若有,空位-1,否则阻塞等待);
        P(互斥锁-井);
        到水井打水;
        V(互斥锁-井);
        V(水井空位+1);
        
        P(水缸是否有空位?若有,空位-1,否则阻塞等待);
        P(互斥锁-水缸);
        往缸里倒水;
        V(互斥锁-水缸);
        V(桶+1);
        V(水+1);
    }
}

老和尚(){
    while(1){
        P(是否有桶?若有,桶-1,否则阻塞等待);
        P(互斥锁-桶);
        取桶;
        V(互斥锁-桶);
        
        P(水缸是否有水?若有,水-1,否则阻塞等待);
        P(互斥锁-水缸);
        到水缸打水;
        V(互斥锁-水缸);
        V(桶+1);
        V(水缸空位+1);
    }
}

Step 4. 检查是否出现死锁

两类进程都可能会出现死锁。原因在于,小和尚取桶、水井取水、往水缸倒水和检查的三个 P 操作不能一气呵成,先检查一类资源,再去占用,容易发生死锁;同理,老和尚取桶、从水缸取水也不能一气呵成。如果先检查所有的资源是否满足,再去占用资源(参考银行家算法),那就不会发生死锁。

事实上,我们可以将小和尚检查是否有桶、水井是否有空位、水缸是否有空位这三个 P 操作连在一起,也就是说,先让小和尚一并检查这三类资源是否满足需求,如果满足,再让他占用资源,完成下面的操作。这个原理,其中之一就是破坏请求保持条件,在进程运行前一次申请完所有的资源;其中之二是银行家算法的思想——避免死锁。这也说明要多去回顾课本,有些知识会我们有所启发。对老和尚也是一样的道理。当然,这些连续的 P 操作必须要加上一个互斥大锁。

小和尚(){
    while(1){
        P(互斥大锁); //
        P(是否有桶?若有,桶-1,否则阻塞等待);
        P(水井是否有空位?若有,空位-1,否则阻塞等待);
        P(水缸是否有空位?若有,空位-1,否则阻塞等待);
        V(互斥大锁); //
        
        P(互斥锁-桶);
        取桶;
        V(互斥锁-桶);
        
        P(互斥锁-井);
        到水井打水;
        V(互斥锁-井);
        V(水井空位+1);
        
        P(互斥锁-水缸);
        往缸里倒水;
        V(互斥锁-水缸);
        V(桶+1);
        V(水+1);
    }
}

老和尚(){
    while(1){
        P(互斥大锁); //
        P(是否有桶?若有,桶-1,否则阻塞等待);
        P(水缸是否有水?若有,水-1,否则阻塞等待);
        V(互斥大锁); //
        
        P(互斥锁-桶);
        取桶;
        V(互斥锁-桶);
        
        P(互斥锁-水缸);
        到水缸打水;
        V(互斥锁-水缸);
        V(桶+1);
        V(水缸空位+1);
    }
}

Step 5. 定义信号量

把所有 PV 操作的中文全部换成英文即可。由于我们写的是中文,所以需要注意 PV 操作对应的信号量是什么,以及是否相同。

信号量的初始值一般为题目中给出的初始值。互斥信号量的初始值一般为 1。

  • 信号量:初始值应为 3。
  • 水井信号量:初始值应为 1。
  • 水缸的水信号量:初始值应为 0。
  • 水缸的空位信号量:初始值应为 10。
  • 互斥锁-桶信号量:初始值应为 1。
  • 互斥锁-井信号量:初始值应为 1。
  • 互斥锁-水缸信号量:初始值应为 1。
  • 互斥大锁信号量:初始值应为 1。
信号量 tong = 3;
信号量 jing = 1;
信号量 gang_full = 0;
信号量 gang_empty = 10;
信号量 tong_mutex = 1;
信号量 jing_mutex = 1;
信号量 gang_mutex = 1;
信号量 mutex = 1;

小和尚(){
    while(1){
        P(mutex);
        P(tong);
        P(jing);
        P(gang_empty);
        V(mutex);
        
        P(tong_mutex);
        取桶;
        V(tong_mutex);
        
        P(jing_mutex);
        到水井打水;
        V(jing_mutex);
        V(jing);
        
        P(gang_mutex);
        往缸里倒水;
        V(gang_mutex);
        V(tong);
        V(gang_full);
    }
}

老和尚(){
    while(1){
        P(mutex);
        P(tong);
        P(gang_full);
        V(mutex);
        
        P(tong_mutex);
        取桶;
        V(tong_mutex);
        
        P(mutex_gang);
        到水缸打水;
        V(mutex_gang);
        V(tong);
        V(gang_empty);
    }
}

发现水井的信号量为 1,则该信号量实际上也充当了互斥的作用,所以可以去掉。整理一下代码就可以得到最终答案:

信号量 tong = 3;
信号量 jing = 1;
信号量 gang_full = 0;
信号量 gang_empty = 10;
信号量 tong_mutex = 1;
信号量 gang_mutex = 1;
信号量 mutex = 1;

小和尚(){
    while(1){
        P(mutex);
        P(tong);
        P(jing);
        P(gang_empty);
        V(mutex);
        
        P(tong_mutex);
        取桶;
        V(tong_mutex);
        V(tong);
        
        到水井打水;
        V(jing);
        
        P(gang_mutex);
        往缸里倒水;
        V(gang_mutex);
        V(gang_full);
    }
}

老和尚(){
    while(1){
        P(mutex);
        P(tong);
        P(gang_full);
        V(mutex);
        
        P(tong_mutex);
        取桶;
        V(tong_mutex);
        V(tong);
        
        P(mutex_gang);
        到水缸打水;
        V(mutex_gang);
        V(gang_empty);
    }
}
posted @ 2022-10-11 13:52  漫舞八月(Mount256)  阅读(1157)  评论(0编辑  收藏  举报