🔧 C++ 设计模式系列:生产者-消费者模式完全指南
📅 更新时间:2025年10月19日
🏷️ 标签:C++ | 设计模式 | 多线程 | 并发编程 | 音视频开发
文章目录
- [📖 什么是生产者-消费者模式](#📖 什么是生产者-消费者模式)
-
- [1. 生活中的例子](#1. 生活中的例子)
- [2. 程序中的定义](#2. 程序中的定义)
- [3. 模式结构图](#3. 模式结构图)
- [🎯 为什么需要这个模式](#🎯 为什么需要这个模式)
- [🔧 核心组件详解](#🔧 核心组件详解)
-
- [1. 线程安全队列](#1. 线程安全队列)
- [2. 互斥锁(Mutex)](#2. 互斥锁(Mutex))
- [3. 条件变量(Condition Variable)](#3. 条件变量(Condition Variable))
- [4. 工作流程详解](#4. 工作流程详解)
-
- 生产者放入数据的流程
- 消费者取出数据的流程
- 关键步骤说明
-
- [📌 生产者工作步骤](#📌 生产者工作步骤)
- [📌 消费者工作步骤](#📌 消费者工作步骤)
- [⚠️ 为什么要"重新检查"?](#⚠️ 为什么要"重新检查"?)
- [💻 C++ 完整实现](#💻 C++ 完整实现)
- [⚠️ 常见问题](#⚠️ 常见问题)
- [🎓 【拓展】在音视频播放器中的应用](#🎓 【拓展】在音视频播放器中的应用)
-
- [1. 经典三线程架构](#1. 经典三线程架构)
- [📋 总结](#📋 总结)
📖 什么是生产者-消费者模式
1. 生活中的例子
想象一个汉堡店:
厨师(生产者) → [ 出餐台 ] → 服务员(消费者)
做汉堡 (缓冲区) 取汉堡给顾客
关键点:
- 厨师做汉堡的速度 ≠ 服务员取汉堡的速度
- 出餐台是缓冲区,平衡两者速度差
- 厨师不用等服务员,服务员不用催厨师
2. 程序中的定义
生产者-消费者模式是一种经典的多线程协作模式:
生产者线程 → [ 共享队列 ] → 消费者线程
生产数据 缓冲区 消费数据
核心要素:
- 生产者(Producer):生产数据并放入队列
- 消费者(Consumer):从队列取数据并处理
- 缓冲区(Buffer):线程安全的共享队列
- 同步机制:互斥锁 + 条件变量
3. 模式结构图
push push pop pop 生产者线程1 线程安全队列 生产者线程2 消费者线程1 消费者线程2
🎯 为什么需要这个模式
问题场景
❌ 没有生产者-消费者模式
cpp
// 直接调用,生产和消费耦合在一起
void process_data() {
while(true) {
Data data = produce(); // 生产数据
consume(data); // 立即消费
}
}
缺点:
- ❌ 生产和消费必须同步,速度慢的拖累速度快的
- ❌ 如果消费很慢,生产者必须等待
- ❌ 无法利用多核CPU并行处理
✅ 使用生产者-消费者模式
下面这个示例先不考虑线程安全问题!!!
cpp
// 生产者线程
void producer_thread() {
while(true) {
Data data = produce();
queue.push(data); // 放入队列就继续
}
}
// 消费者线程
void consumer_thread() {
while(true) {
Data data = queue.pop();
consume(data); // 独立消费
}
}
优点:
- ✅ 解耦:生产和消费独立,互不影响
- ✅ 缓冲:队列平滑速率差异
- ✅ 并行:多个生产者/消费者并行工作
- ✅ 灵活:可以动态调整生产/消费线程数
适用场景
场景 | 生产者 | 消费者 | 缓冲区 |
---|---|---|---|
视频播放器 | 解码线程 | 渲染线程 | 解码帧队列 |
网络下载 | 下载线程 | 写文件线程 | 数据块队列 |
日志系统 | 业务线程 | 写日志线程 | 日志消息队列 |
任务调度 | 任务分发器 | 工作线程 | 任务队列 |
数据处理 | 数据采集 | 数据分析 | 原始数据队列 |
🔧 核心组件详解
1. 线程安全队列
为什么需要线程安全?
cpp
// ❌ 非线程安全的队列
std::queue<int> q;
// 线程A
q.push(1);
// 线程B(同时执行)
q.push(2);
// 可能导致:数据竞争、队列损坏、程序崩溃
线程安全的要求
1. 互斥访问:同一时刻只有一个线程操作队列
2. 条件等待:队列空时消费者等待,队列满时生产者等待
3. 通知机制:有数据时唤醒消费者,有空间时唤醒生产者
2. 互斥锁(Mutex)
作用:保证同一时刻只有一个线程访问共享资源
cpp
std::mutex mtx;
// 加锁
mtx.lock();
// 临界区:只有一个线程能执行到这里
q.push(data);
mtx.unlock();
// 更安全的方式:自动加锁/解锁
{
std::lock_guard<std::mutex> lock(mtx);
q.push(data); // 离开作用域自动解锁
}
3. 条件变量(Condition Variable)
作用:让线程等待某个条件成立
cpp
std::condition_variable cv;
// 等待(释放锁并睡眠,直到被唤醒)
cv.wait(lock, [](){ return !queue.empty(); });
// 通知一个等待的线程
cv.notify_one();
// 通知所有等待的线程
cv.notify_all();
4. 工作流程详解
生产者放入数据的流程
是 否 生产者开始 获取互斥锁 队列是否满? 释放锁并等待
进入睡眠状态 被消费者唤醒
重新获取锁 将数据放入队列 通知消费者
有新数据了 释放互斥锁 生产者结束
消费者取出数据的流程
是 否 消费者开始 获取互斥锁 队列是否空? 释放锁并等待
进入睡眠状态 被生产者唤醒
重新获取锁 从队列取出数据 通知生产者
有空间了 释放互斥锁 处理数据 消费者结束
关键步骤说明
📌 生产者工作步骤
步骤1:获取互斥锁(确保独占访问队列)
步骤2:检查队列是否满
├─ 如果满了 → 释放锁,睡眠等待(不占用CPU)
│ → 被消费者唤醒后,重新检查
└─ 如果没满 → 继续
步骤3:将数据放入队列
步骤4:通知消费者"有新数据了"
步骤5:释放互斥锁
📌 消费者工作步骤
步骤1:获取互斥锁(确保独占访问队列)
步骤2:检查队列是否空
├─ 如果空了 → 释放锁,睡眠等待(不占用CPU)
│ → 被生产者唤醒后,重新检查
└─ 如果不空 → 继续
步骤3:从队列取出数据
步骤4:通知生产者"有空间了"
步骤5:释放互斥锁
步骤6:处理数据
⚠️ 为什么要"重新检查"?
关键点:被唤醒后必须重新检查条件!
cpp
// ❌ 错误:不重新检查
cv.wait(lock);
// 可能被虚假唤醒,或者数据已被其他消费者取走
T value = queue.front(); // 危险!
// ✅ 正确:使用 lambda 自动重新检查
cv.wait(lock, [](){ return !queue.empty(); });
// 只有条件满足才返回
T value = queue.front(); // 安全
原因:
- 可能有虚假唤醒(操作系统特性)
- 可能有多个消费者同时被唤醒,其中一个先取走了数据
- 必须确保条件真正满足才继续执行
💻 C++ 完整实现
cpp
#include <iostream>
#include <queue>
#include <mutex>
#include <condition_variable>
#include <thread>
#include <atomic>
#include <chrono>
std::queue<int> q;
std::mutex mtx;
std::condition_variable cv;
std::atomic<bool> finished{false};
// 生产者线程
std::thread producer([]() {
for (int i = 1; i <= 9; i++) {
{
std::lock_guard<std::mutex> lock(mtx);
q.push(i);
std::cout << "[生产] " << i << " | 队列: " << q.size() << std::endl; // ← 添加输出
}
cv.notify_one();
std::this_thread::sleep_for(std::chrono::milliseconds(100)); // ← 添加延迟
}
finished = true;
cv.notify_all();
std::cout << "[生产者] 完成\n" << std::endl;
});
// 消费者线程
std::thread consumer([]() {
int count = 0; // ← 统计消费数量
while (true) {
int value;
{
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, []() {
return !q.empty() || finished;
});
if(finished && q.empty()) {
break;
}
value = q.front();
q.pop();
count++; // ← 计数
}
std::cout << " [消费] " << value
<< " | 队列: " << q.size()
<< " | 已消费: " << count << std::endl; // ← 添加输出
std::this_thread::sleep_for(std::chrono::milliseconds(100)); // ← 添加延迟
}
std::cout << " [消费者] 完成,共消费 " << count << " 个\n" << std::endl;
});
int main() {
producer.join();
consumer.join();
return 0;
}

⚠️ 常见问题
问题1:虚假唤醒(Spurious Wakeup)
什么是虚假唤醒?
cpp
// ❌ 错误:使用 if 判断条件
cv.wait(lock);
if(!queue.empty()) { // 可能被虚假唤醒,条件不满足
T value = queue.front();
}
// ✅ 正确:使用 while 或 lambda 判断条件
cv.wait(lock, [](){ return !queue.empty(); });
原因:
- 操作系统可能在没有
notify
的情况下唤醒线程 - 多个线程同时被唤醒,其中一个消费了数据
- 必须重新检查条件
问题2:死锁
常见死锁场景
cpp
// ❌ 错误:忘记解锁
void push(T value) {
mutex_.lock();
queue_.push(value);
// 忘记 unlock,导致其他线程永久等待
}
// ✅ 正确:使用 RAII 自动管理锁
void push(T value) {
std::lock_guard<std::mutex> lock(mutex_);
queue_.push(value);
} // 离开作用域自动解锁
🎓 【拓展】在音视频播放器中的应用
1. 经典三线程架构
视频Packet 音频Packet 解封装线程
Demuxer 视频解码队列 音频解码队列 视频解码线程
Video Decoder 音频解码线程
Audio Decoder 视频帧队列
Video Frame Queue 音频帧队列
Audio Frame Queue 视频渲染线程
Render 音频播放线程
Audio Player
📋 总结
核心要点
1️⃣ 生产者-消费者模式 = 生产者 + 消费者 + 线程安全队列
2️⃣ 必须使用互斥锁保证线程安全
3️⃣ 必须使用条件变量实现等待/通知
4️⃣ 注意虚假唤醒,使用 lambda 判断条件
5️⃣ 使用 RAII(lock_guard)避免死锁
优缺点对比
✅ 优点
1. 解耦:生产和消费独立
2. 缓冲:平滑速率波动
3. 并发:充分利用多核
4. 灵活:易于扩展
5. 可靠:队列作为缓冲区
⚠️ 注意事项
1. 有同步开销(锁竞争)
2. 需要合理设置队列大小
3. 注意内存管理(特别是指针)
4. 需要优雅停止机制
5. 调试相对复杂
使用场景总结
场景类型 | 是否适用 | 说明 |
---|---|---|
多线程任务分发 | ✅ 非常适合 | 经典应用场景 |
异步IO | ✅ 适合 | 读写分离 |
音视频处理 | ✅ 非常适合 | 解码、渲染分离 |
日志系统 | ✅ 适合 | 业务与IO分离 |
单线程程序 | ❌ 不适用 | 无并发需求 |
实时性要求极高 | ⚠️ 慎用 | 锁开销可能不可接受 |
如果您觉得这篇文章对您有帮助,不妨点赞 + 收藏 + 关注,更多设计模式系列教程将持续更新 🔥!