C++ 设计模式系列:生产者-消费者模式完全指南

🔧 C++ 设计模式系列:生产者-消费者模式完全指南

📅 更新时间:2025年10月19日

🏷️ 标签:C++ | 设计模式 | 多线程 | 并发编程 | 音视频开发

文章目录

  • [📖 什么是生产者-消费者模式](#📖 什么是生产者-消费者模式)
    • [1. 生活中的例子](#1. 生活中的例子)
    • [2. 程序中的定义](#2. 程序中的定义)
    • [3. 模式结构图](#3. 模式结构图)
  • [🎯 为什么需要这个模式](#🎯 为什么需要这个模式)
    • 问题场景
      • [❌ 没有生产者-消费者模式](#❌ 没有生产者-消费者模式)
      • [✅ 使用生产者-消费者模式](#✅ 使用生产者-消费者模式)
    • 适用场景
  • [🔧 核心组件详解](#🔧 核心组件详解)
  • [💻 C++ 完整实现](#💻 C++ 完整实现)
  • [⚠️ 常见问题](#⚠️ 常见问题)
  • [🎓 【拓展】在音视频播放器中的应用](#🎓 【拓展】在音视频播放器中的应用)
    • [1. 经典三线程架构](#1. 经典三线程架构)
  • [📋 总结](#📋 总结)

📖 什么是生产者-消费者模式

1. 生活中的例子

想象一个汉堡店:

复制代码
厨师(生产者)  →  [ 出餐台 ]  →  服务员(消费者)
   做汉堡           (缓冲区)        取汉堡给顾客

关键点

  • 厨师做汉堡的速度 ≠ 服务员取汉堡的速度
  • 出餐台是缓冲区,平衡两者速度差
  • 厨师不用等服务员,服务员不用催厨师

2. 程序中的定义

生产者-消费者模式是一种经典的多线程协作模式:

复制代码
生产者线程  →  [ 共享队列 ]  →  消费者线程
  生产数据        缓冲区         消费数据

核心要素

  1. 生产者(Producer):生产数据并放入队列
  2. 消费者(Consumer):从队列取数据并处理
  3. 缓冲区(Buffer):线程安全的共享队列
  4. 同步机制:互斥锁 + 条件变量

3. 模式结构图

push push pop pop 生产者线程1 线程安全队列 生产者线程2 消费者线程1 消费者线程2


🎯 为什么需要这个模式

问题场景

❌ 没有生产者-消费者模式

cpp 复制代码
// 直接调用,生产和消费耦合在一起
void process_data() {
    while(true) {
        Data data = produce();      // 生产数据
        consume(data);              // 立即消费
    }
}

缺点

  1. ❌ 生产和消费必须同步,速度慢的拖累速度快的
  2. ❌ 如果消费很慢,生产者必须等待
  3. ❌ 无法利用多核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. 解耦:生产和消费独立,互不影响
  2. 缓冲:队列平滑速率差异
  3. 并行:多个生产者/消费者并行工作
  4. 灵活:可以动态调整生产/消费线程数

适用场景

场景 生产者 消费者 缓冲区
视频播放器 解码线程 渲染线程 解码帧队列
网络下载 下载线程 写文件线程 数据块队列
日志系统 业务线程 写日志线程 日志消息队列
任务调度 任务分发器 工作线程 任务队列
数据处理 数据采集 数据分析 原始数据队列

🔧 核心组件详解

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();  // 安全

原因

  1. 可能有虚假唤醒(操作系统特性)
  2. 可能有多个消费者同时被唤醒,其中一个先取走了数据
  3. 必须确保条件真正满足才继续执行

💻 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分离
单线程程序 ❌ 不适用 无并发需求
实时性要求极高 ⚠️ 慎用 锁开销可能不可接受

如果您觉得这篇文章对您有帮助,不妨点赞 + 收藏 + 关注,更多设计模式系列教程将持续更新 🔥!

相关推荐
liliangcsdn3 小时前
python如何写数据到excel示例
开发语言·python·excel
workflower6 小时前
单元测试-例子
java·开发语言·算法·django·个人开发·结对编程
YuanlongWang6 小时前
C# 基础——装箱和拆箱
java·开发语言·c#
b78gb7 小时前
电商秒杀系统设计 Java+MySQL实现高并发库存管理与订单处理
java·开发语言·mysql
LXS_3578 小时前
Day 05 C++ 入门 之 指针
开发语言·c++·笔记·学习方法·改行学it
etsuyou9 小时前
js前端this指向规则
开发语言·前端·javascript
shizhenshide10 小时前
为什么有时候 reCAPTCHA 通过率偏低,常见原因有哪些
开发语言·php·验证码·captcha·recaptcha·ezcaptcha
挂科是不可能出现的10 小时前
最长连续序列
数据结构·c++·算法
mit6.82410 小时前
[Agent可视化] 配置系统 | 实现AI模型切换 | 热重载机制 | fsnotify库(go)
开发语言·人工智能·golang