第十七课:生产者-消费者问题(Producer-Consumer Problem)


一、什么是生产者-消费者?

先来看一个生活例子。

假设:

有一家奶茶店。

员工:

负责:

做奶茶。

顾客:

负责:

拿奶茶。

中间:

有一个:

奶茶架。

员工(生产者)

↓

【奶茶架】

↓

顾客(消费者)

员工:

一直:

做奶茶。

顾客:

一直:

拿奶茶。

是不是:

很合理?


二、为什么会出问题?

假设:

奶茶架:

只能:

放:

3 杯奶茶。

开始:

[ ][ ][ ]

员工:

一直做。

[奶][ ][ ]

继续:

[奶][奶][ ]

继续:

[奶][奶][奶]

已经:

满了。

如果:

员工:

还继续:

放。

是不是:

放不下?

所以:

必须:

等待。


另一边。

如果:

奶茶架:

空了。

[ ][ ][ ]

顾客:

还来拿。

是不是:

拿不到?

所以:

消费者:

也必须:

等待。


三、操作系统里的对应关系

奶茶店:

对应:

程序。

奶茶店操作系统
员工生产者线程
顾客消费者线程
奶茶架缓冲区(Buffer)
奶茶数据

缓冲区:

就是:

一块:

共享内存。


四、缓冲区会出现两个问题

假设:

缓冲区:

大小:

3

现在:

已经:

满了。

生产者:

还能:

放吗?

不能。

必须:

等待。


另一种。

缓冲区:

为空。

消费者:

还能:

取吗?

不能。

必须:

等待。

所以:

这里:

需要:

同步。


另外。

如果:

两个生产者:

同时:

往:

同一个位置:

写数据。

是不是:

又会:

覆盖?

两个消费者:

同时:

取:

同一个数据。

是不是:

又会:

重复?

所以:

还需要:

互斥。


一个总结

生产者-消费者问题里,同时存在两类需求:

  1. 同步 :控制"什么时候能生产、什么时候能消费"。
  2. 互斥 :保证同一时刻只有一个线程修改缓冲区。

很多初学者只想到同步,忘了互斥,这是最容易丢分的地方。


五、需要几个信号量?

这是考试:

第一问。

答案:

三个。


① mutex

作用:

保护:

缓冲区。

初值:

1

因为:

一次:

只能:

一个线程:

操作。


② empty

表示:

空位置。

例如:

缓冲区:

大小:

5。

开始:

empty = 5

说明:

还有:

5 个位置。


③ full

表示:

已有数据。

开始:

full = 0

因为:

一开始:

没有:

任何数据。


六、三个信号量总结

信号量初值含义
mutex1互斥访问缓冲区
emptyn(缓冲区大小)空缓冲区数量
full0已放入数据数量

这是考试必背。


七、生产者应该怎么写?

假设:

生产:

一个数据。

第一步:

是不是:

先看:

有没有:

空位置?

所以:

第一句:

P(empty);

意思:

申请:

一个空位置。


第二步。

是不是:

还要:

锁门?

因为:

别人:

不能:

同时:

改缓冲区。

于是:

P(mutex);

第三步。

真正:

放数据。

put();

第四步。

放完。

是不是:

解锁?

V(mutex);

第五步。

缓冲区:

多了:

一个数据。

消费者:

可以:

拿了。

于是:

V(full);

完整代码:

P(empty);
P(mutex);

放入数据;

V(mutex);
V(full);

八、消费者怎么写?

第一步。

有没有:

数据?

所以:

先:

P(full);

没有数据:

就等待。


第二步。

锁门。

P(mutex);

第三步。

真正:

拿数据。

get();

第四步。

解锁。

V(mutex);

第五步。

空位置:

多了:

一个。

于是:

V(empty);

完整:

P(full);
P(mutex);

取数据;

V(mutex);
V(empty);

九、为什么顺序不能写反?

来看一个经典错误。

有人:

写:

P(mutex);

P(empty);

这样:

可以吗?

不可以。

为什么?

假设:

缓冲区:

已经:

满了。

生产者:

执行:

P(mutex)

拿到了:

锁。

接着:

执行:

P(empty)

发现:

没有:

空位。

于是:

睡觉。

注意!

它:

睡觉的时候:

还拿着:

锁。

消费者:

想:

拿数据。

第一句:

P(full)

成功。

第二句:

P(mutex)

发现:

锁:

没了。

消费者:

也睡觉。

结果:

生产者:

等消费者

↓

消费者:

等生产者

是不是:

谁也:

醒不过来?

这就是:

死锁(Deadlock)

十、为什么正确顺序不会死锁?

正确:

一定:

先:

P(empty)

资源:

够。

再:

P(mutex)

所以:

不会:

拿着:

锁:

睡觉。

这一点:

考试:

经常:

考。


十一、一张图理解

生产者

        P(empty)

        P(mutex)

      放入数据

        V(mutex)

        V(full)

-----------------------

            消费者

        P(full)

        P(mutex)

      取出数据

        V(mutex)

        V(empty)

建议你把这张图手写一遍,记忆效果最好。


十二、考试最喜欢问的问题

问:

为什么:

需要:

三个信号量?

答案:

信号量作用
empty保证不会放满
full保证不会取空
mutex保证互斥

问:

为什么:

mutex:

初值:

1?

因为:

一次:

一个线程。


问:

为什么:

full:

初值:

0?

因为:

开始:

没有:

数据。


问:

为什么:

empty:

初值:

n?

因为:

开始:

全部:

都是:

空位。


十三、口诀(★★★★★)

考试前:

一定背。

生产者:

P(empty)

↓

P(mutex)

↓

生产

↓

V(mutex)

↓

V(full)

消费者:

P(full)

↓

P(mutex)

↓

消费

↓

V(mutex)

↓

V(empty)

口诀:

生产先空后锁,消费先满后锁;解锁之后再通知。

这句话能帮助你记住正确的 P/V 操作顺序。


十四、本课重点(★★★★★)

必须掌握:

✅ 三个信号量:

  • mutex = 1
  • empty = n
  • full = 0

✅ 生产者:

P(empty);
P(mutex);

生产;

V(mutex);
V(full);

✅ 消费者:

P(full);
P(mutex);

消费;

V(mutex);
V(empty);

✅ 资源信号量(empty/full)要先申请,再申请互斥锁;释放时先释放互斥锁,再通知资源变化。

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐