操作系统里的生产者消费者问题
·
记录一下这个经典的多线程同步模型
一、这是个什么问题?
说白了就是两个角色在共享一个固定大小的缓冲区:
- 生产者:往缓冲区里放数据
- 消费者:从缓冲区里拿数据
听起来简单,但实际运行起来有两个核心矛盾:
- 缓冲区满了,生产者还往里放 → 数据丢失
- 缓冲区空了,消费者还来拿 → 拿不到数据
二、一个具体场景
想象一个仓库(缓冲区),容量是10件货物:
┌─────────────────────────────────────────┐
│ [1] [2] [3] [4] [5] [6] [7] [8] [9] [10] │ ← 仓库已满
└─────────────────────────────────────────┘
↑ ↑
生产者想放第11件 消费者来取
✗ ✓
阻塞等待 正常取走
正常运转的情况:
- 仓库没满 → 生产者随便放
- 仓库没空 → 消费者随便取
出问题的情况:
- 仓库满了 → 生产者必须等待消费者取走一些
- 仓库空了 → 消费者必须等待生产者放进新的
三、需要解决什么问题?
多线程环境下,主要有三个坑:
坑1:竞态条件
两个生产者同时往同一个空位放数据,会相互覆盖。
生产者A:读取count=5,准备往位置5放
生产者B:读取count=5,也准备往位置5放 ← 同时发生
生产者A:放入数据A
生产者B:放入数据B ← 把A覆盖了
坑2:忙等待(浪费CPU)
// 消费者代码(错误示范)
while (count == 0) {
// 空转,疯狂检查,CPU占用100%
continue;
}
// 取数据
坑3:死锁
拿锁的顺序不对,两个线程互相等对方释放资源,永远卡住。
四、操作系统怎么解决?
4.1 用信号量(Semaphore)
信号量就是一个计数器,两个关键操作:
P(S):资源减1,如果不够就阻塞等待V(S):资源加1,唤醒等待的线程
具体实现:
// 初始化
semaphore mutex = 1; // 互斥锁,保护缓冲区
semaphore empty = n; // 空位数量,初始=n
semaphore full = 0; // 已占用的数量,初始=0
// 生产者
void producer() {
while(1) {
item = produce_item(); // 生产数据
P(&empty); // 等一个空位
P(&mutex); // 加锁,进入临界区
insert_item(item); // 放数据
V(&mutex); // 解锁
V(&full); // 通知消费者:多了一个货
}
}
// 消费者
void consumer() {
while(1) {
P(&full); // 等有货
P(&mutex); // 加锁
item = remove_item(); // 取数据
V(&mutex); // 解锁
V(&empty); // 通知生产者:空出一个位置
consume_item(item); // 消费数据
}
}
4.2 图示流程
初始状态:empty=3, full=0, mutex=1
生产者P1执行:
P(empty) → empty=2
P(mutex) → mutex=0
放入数据
V(mutex) → mutex=1
V(full) → full=1
此时缓冲区:[A] [_] [_]
消费者C1执行:
P(full) → full=0
P(mutex) → mutex=0
取出A
V(mutex) → mutex=1
V(empty) → empty=3
缓冲区:[_] [_] [_]
4.3 用管程(Monitor)
信号量写法容易出错,管程是更高级的封装,把共享资源和操作方法包在一起。
伪代码:
monitor ProducerConsumer {
condition full, empty;
int count = 0;
Item buffer[N];
void put(Item item) {
if (count == N)
wait(full); // 满了就等
buffer[in] = item;
count++;
signal(empty); // 通知消费者
}
Item get() {
if (count == 0)
wait(empty); // 空了就等
Item item = buffer[out];
count--;
signal(full); // 通知生产者
return item;
}
}
Java 里用 synchronized + wait/notify 就是管程的实现。
五、几种变体
单生产者-单消费者(最简单)
- 只需要两个信号量:empty、full
- 不需要互斥锁,因为只有一个生产者和一个消费者,不会冲突
多生产者-多消费者(常见)
- 必须加互斥锁
mutex - 多个生产者可能同时写同一个位置
有界缓冲区 vs 无界缓冲区
| 类型 | 区别 | 风险 |
|---|---|---|
| 有界 | 缓冲区大小固定 | 满了必须阻塞生产者 |
| 无界 | 理论上无限大 | 消费者跟不上时,内存会爆 |
实际生产中,一定有界,防止内存被撑爆。
六、和业务开发的区别
| 对比项 | 操作系统层面 | 业务开发(消息队列) |
|---|---|---|
| 缓冲区 | 共享内存数组 | Redis、Kafka、RabbitMQ |
| 同步机制 | 信号量、管程、互斥锁 | ACK确认、offset、持久化 |
| 阻塞方式 | 线程挂起/唤醒 | 长轮询、回调 |
| 容错 | 程序崩了就没了 | 消息持久化、重试机制 |
一句话概括:
OS的模型解决的是多线程访问共享内存的问题
消息队列解决的是分布式系统里服务间通信的问题
本质逻辑一样,但工程实现差很多。
七、手绘一个完整流程图
┌─────────────┐
│ 生产者线程1 │
└──────┬──────┘
┌──────┴──────┐
│ 生产者线程2 │
└──────┬──────┘
│
▼ P(empty) 等待空位
┌─────────────┐
│ P(mutex) │ ← 互斥锁,一次只能一个进
└──────┬──────┘
│
▼
╔═══════════════════╗
║ 缓冲区(有限) ║
║ [ ][A][B][ ][ ] ║
╚═══════════════════╝
│
▼ V(mutex) + V(full)
┌─────────────┐
│ 唤醒消费者 │
└─────────────┘
│
▼
┌─────────────┐ ┌─────────────┐
│ 消费者线程1 │ │ 消费者线程2 │
└─────────────┘ └─────────────┘
八、思考题(面试常见)
-
为什么需要三个信号量?
mutex:保护缓冲区的互斥访问empty:生产者等空位full:消费者等数据
-
交换P(mutex)和P(empty)的顺序会怎样?
先拿锁再等空位,如果缓冲区满了,生产者会拿着锁等消费者,而消费者拿不到锁 → 死锁 -
单生产单消费需要mutex吗?
不需要,但前提是生产者和消费者各自只有一个线程。多个就要。
九、总结

这个模型的本质是:
用阻塞替代忙等,用计数器控制流量,用互斥锁防止冲突
核心代码就是那两段 P/V 操作,弄明白这个,再去理解 Kafka、RabbitMQ 的设计思路会轻松很多。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐

所有评论(0)