【Linux】线程同步与互斥(三):生产者消费者模型实战

文章目录

    • Linux线程同步与互斥(三):生产者消费者模型实战
    • 一、生产者消费者模型
      • [1.1 模型的三要素(3-2-1原则)](#1.1 模型的三要素(3-2-1原则))
      • [1.2 为什么需要这个模型](#1.2 为什么需要这个模型)
      • [1.3 模型的应用场景](#1.3 模型的应用场景)
    • 二、阻塞队列实现
      • [2.1 BlockingQueue设计思路](#2.1 BlockingQueue设计思路)
      • [2.2 完整代码实现](#2.2 完整代码实现)
      • [2.3 测试代码](#2.3 测试代码)
        • [2.3.1 单生产单消费](#2.3.1 单生产单消费)
        • [2.3.2 多生产多消费](#2.3.2 多生产多消费)
    • 三、POSIX信号量
      • [3.1 信号量是什么](#3.1 信号量是什么)
      • [3.2 POSIX信号量API](#3.2 POSIX信号量API)
      • [3.3 信号量 vs 条件变量](#3.3 信号量 vs 条件变量)
      • [3.4 信号量的封装](#3.4 信号量的封装)
    • 四、环形队列实现
      • [4.1 环形队列的设计](#4.1 环形队列的设计)
      • [4.2 信号量控制](#4.2 信号量控制)
      • [4.3 为什么还需要锁](#4.3 为什么还需要锁)
      • [4.4 完整实现](#4.4 完整实现)
      • [4.5 测试代码](#4.5 测试代码)
    • 五、两种实现的对比
      • [5.1 实现对比](#5.1 实现对比)
    • 六、一个完整的生产消费案例
      • [6.1 任务类定义](#6.1 任务类定义)
      • [6.2 完整测试](#6.2 完整测试)
    • 七、本篇总结
      • [7.1 核心知识点](#7.1 核心知识点)
      • [7.2 两种实现选择](#7.2 两种实现选择)
      • [7.3 最重要的理解](#7.3 最重要的理解)

Linux线程同步与互斥(三):生产者消费者模型实战

💬 重磅来袭 :前两篇把互斥锁和条件变量的原理、用法都讲清楚了。但这些工具怎么用来解决实际问题?这就是本篇的核心------生产者-消费者模型(Producer-Consumer)。这是并发编程中最经典的模型,没有之一。从操作系统的进程调度、到消息队列、到线程池任务分发,到处都能看到它的影子。我们会从模型的三要素讲起,实现基于阻塞队列的版本,然后引入POSIX信号量,用环形队列实现一个更高效的版本。通过这两个实现,把前面学的互斥、同步知识全部串起来,形成完整的并发编程能力。

👍 点赞、收藏与分享:本篇包含大量完整代码、多种实现方案对比、实战技巧,是学习并发编程的必读内容!如果对你有帮助,请点赞、收藏并分享!

🚀 循序渐进:从模型原理到代码实现,从简单到复杂,一步步掌握生产者消费者模式。


一、生产者消费者模型

1.1 模型的三要素(3-2-1原则)

生产者消费者模型是一个协作框架,包含三个核心要素:

bash 复制代码
3个角色:
├─ 生产者(Producer):生产数据
├─ 消费者(Consumer):消费数据
└─ 交易场所(容器):存放数据的缓冲区

2种关系:
├─ 生产者与消费者:互斥访问容器,同步
└─ 生产者与生产者、消费者与消费者:也要互斥

1个核心:
└─ 缓冲区(容器):解耦生产者和消费者

基本流程:

bash 复制代码
生产者                  容器                  消费者
  │                    ┌───┐                   │
  ├─ 生产数据           │   │                   │
  │                    │   │                   │
  ├─ 放入容器 ────────> │[1]│                   │
  │                    │   │                   │
  ├─ 继续生产           │[2]│                   │
  │                    │   │                   │
  ├─ 放入容器 ────────> │[3]│                   │
  │                    │   │<──────── 取数据 ──┤
  │                    │   │                   │
  │                    │[4]│                   ├─ 消费数据
  │                    │   │<──────── 取数据 ──┤
  │                    └───┘                   │
  ↓                                            ↓

1.2 为什么需要这个模型

看一个反例,如果没有中间容器会怎样:

cpp 复制代码
// 生产者直接调用消费者处理
void producer() {
    while (1) {
        Data data = produce();
        consumer_process(data);  // 直接调用消费者的函数
    }
}

问题:

bash 复制代码
问题1:紧耦合
  - 生产者必须知道消费者的处理函数
  - 消费者逻辑变化,生产者也要改
  - 无法灵活更换消费者

问题2:效率低
  - 生产者要等消费者处理完才能继续
  - 如果消费者慢,生产者也被拖慢
  - 无法发挥多核优势

问题3:不支持多消费者
  - 一个生产者只能对接一个消费者
  - 无法实现负载均衡

加入容器后:

bash 复制代码
优点1:解耦
  - 生产者只管往容器放数据
  - 消费者只管从容器取数据
  - 双方不需要知道对方的存在

优点2:并发,提高效率
  - 生产者生产完立即放入容器,继续生产
  - 消费者随时从容器取数据处理
  - 充分利用多核CPU

优点3:削峰填谷,支持忙闲不均
  - 生产速度快时,数据堆积在容器
  - 消费速度慢时,容器缓冲避免阻塞
  - 平衡生产和消费的速度差异

📌 核心思想:通过引入缓冲区(容器),把生产和消费两个过程解耦,让它们可以独立、并发地运行。

1.3 模型的应用场景

bash 复制代码
场景1:操作系统进程调度
  - 生产者:创建进程
  - 容器:就绪队列
  - 消费者:CPU调度器

场景2:线程池
  - 生产者:提交任务的线程
  - 容器:任务队列
  - 消费者:工作线程

场景3:消息队列
  - 生产者:发送消息的服务
  - 容器:MQ(如Kafka、RabbitMQ)
  - 消费者:接收消息的服务

场景4:日志系统
  - 生产者:业务线程
  - 容器:日志缓冲区
  - 消费者:日志写入线程

场景5:网络I/O
  - 生产者:网络接收线程
  - 容器:接收缓冲区
  - 消费者:业务处理线程

二、阻塞队列实现

2.1 BlockingQueue设计思路

阻塞队列(Blocking Queue)是实现生产者消费者模型最直接的方式。它的特点:

bash 复制代码
核心特性:
1. 当队列满时,生产者阻塞等待
2. 当队列空时,消费者阻塞等待
3. 线程安全,内部用锁保护

需要的工具:
├─ mutex:保护队列的互斥访问
├─ cond_producer:生产者等待的条件变量(队列满时等待)
└─ cond_consumer:消费者等待的条件变量(队列空时等待)

为什么需要两个条件变量?

bash 复制代码
如果只用一个:
- 队列满时,生产者 wait
- 队列空时,消费者 wait
- 消费者消费后 signal,可能唤醒另一个消费者(不是生产者)
- 生产者生产后 signal,可能唤醒另一个生产者(不是消费者)
- 导致该等待的继续等待,不该等待的被唤醒

用两个条件变量:
- cond_producer:生产者等待,消费者唤醒
- cond_consumer:消费者等待,生产者唤醒
- 角色明确,不会唤醒错对象

2.2 完整代码实现

BlockingQueue.hpp:

cpp 复制代码
#pragma once
#include <iostream>
#include <queue>
#include <pthread.h>

const int default_capacity = 5;

template <typename T>
class BlockingQueue
{
public:
    BlockingQueue(int capacity = default_capacity)
        : _capacity(capacity)
    {
        pthread_mutex_init(&_mutex, nullptr);
        pthread_cond_init(&_cond_producer, nullptr);
        pthread_cond_init(&_cond_consumer, nullptr);
    }

    // 生产者调用:向队列放入数据
    void Push(const T &data)
    {
        pthread_mutex_lock(&_mutex);
        
        // 队列满了,等待消费者消费
        while (IsFull()) {
            // 打印日志,方便观察
            std::cout << "队列满了,生产者等待..." << std::endl;
            pthread_cond_wait(&_cond_producer, &_mutex);
        }
        
        // 队列未满,放入数据
        _queue.push(data);
        
        // 唤醒可能在等待的消费者
        pthread_cond_signal(&_cond_consumer);
        
        pthread_mutex_unlock(&_mutex);
    }

    // 消费者调用:从队列取出数据
    void Pop(T *data)
    {
        pthread_mutex_lock(&_mutex);
        
        // 队列空了,等待生产者生产
        while (IsEmpty()) {
            std::cout << "队列空了,消费者等待..." << std::endl;
            pthread_cond_wait(&_cond_consumer, &_mutex);
        }
        
        // 队列非空,取出数据
        *data = _queue.front();
        _queue.pop();
        
        // 唤醒可能在等待的生产者
        pthread_cond_signal(&_cond_producer);
        
        pthread_mutex_unlock(&_mutex);
    }

    ~BlockingQueue()
    {
        pthread_mutex_destroy(&_mutex);
        pthread_cond_destroy(&_cond_producer);
        pthread_cond_destroy(&_cond_consumer);
    }

private:
    bool IsFull()
    {
        return _queue.size() == _capacity;
    }

    bool IsEmpty()
    {
        return _queue.empty();
    }

private:
    std::queue<T> _queue;       // 底层容器
    int _capacity;              // 容量上限
    pthread_mutex_t _mutex;     // 互斥锁
    pthread_cond_t _cond_producer;  // 生产者条件变量
    pthread_cond_t _cond_consumer;  // 消费者条件变量
};

2.3 测试代码

2.3.1 单生产单消费

test_blocking_queue.cpp:

cpp 复制代码
#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include "BlockingQueue.hpp"

BlockingQueue<int> bq;

void *producer(void *arg)
{
    int id = *(int *)arg;
    while (1) {
        // 生产数据(随机数)
        int data = rand() % 100;
        
        bq.Push(data);
        std::cout << "生产者" << id << ": 生产数据 " << data << std::endl;
        
        sleep(1);  // 模拟生产耗时
    }
    return nullptr;
}

void *consumer(void *arg)
{
    int id = *(int *)arg;
    while (1) {
        int data;
        bq.Pop(&data);
        std::cout << "消费者" << id << ": 消费数据 " << data << std::endl;
        
        sleep(2);  // 模拟消费耗时(消费比生产慢)
    }
    return nullptr;
}

int main()
{
    srand(time(nullptr));
    
    pthread_t p, c;
    int pid = 1, cid = 1;
    
    pthread_create(&p, nullptr, producer, &pid);
    pthread_create(&c, nullptr, consumer, &cid);
    
    pthread_join(p, nullptr);
    pthread_join(c, nullptr);
    
    return 0;
}

编译运行:

bash 复制代码
$ g++ test_blocking_queue.cpp -o test -std=c++11 -lpthread
$ ./test
生产者1: 生产数据 83
消费者1: 消费数据 83
生产者1: 生产数据 86
生产者1: 生产数据 77
消费者1: 消费数据 86
生产者1: 生产数据 15
生产者1: 生产数据 93
消费者1: 消费数据 77
生产者1: 生产数据 35
队列满了,生产者等待...
消费者1: 消费数据 15
生产者1: 生产数据 86
队列满了,生产者等待...
消费者1: 消费数据 93
生产者1: 生产数据 92
队列满了,生产者等待...
...

观察:

bash 复制代码
1. 生产速度 > 消费速度(1秒 vs 2秒)
2. 队列很快填满(容量5)
3. 生产者频繁等待
4. 消费者从不等待(队列总有数据)
2.3.2 多生产多消费
cpp 复制代码
int main()
{
    srand(time(nullptr));
    
    pthread_t p1, p2, p3;
    pthread_t c1, c2;
    
    int pid1 = 1, pid2 = 2, pid3 = 3;
    int cid1 = 1, cid2 = 2;
    
    pthread_create(&p1, nullptr, producer, &pid1);
    pthread_create(&p2, nullptr, producer, &pid2);
    pthread_create(&p3, nullptr, producer, &pid3);
    pthread_create(&c1, nullptr, consumer, &cid1);
    pthread_create(&c2, nullptr, consumer, &cid2);
    
    pthread_join(p1, nullptr);
    pthread_join(p2, nullptr);
    pthread_join(p3, nullptr);
    pthread_join(c1, nullptr);
    pthread_join(c2, nullptr);
    
    return 0;
}

运行结果(部分):

bash 复制代码
生产者1: 生产数据 83
生产者2: 生产数据 86
生产者3: 生产数据 77
消费者1: 消费数据 83
消费者2: 消费数据 86
生产者1: 生产数据 15
生产者2: 生产数据 93
消费者1: 消费数据 77
生产者3: 生产数据 35
消费者2: 消费数据 15
...

观察:

bash 复制代码
1. 多个生产者并发生产
2. 多个消费者并发消费
3. 没有数据丢失或重复
4. 线程安全地共享队列

📌 关键点:BlockingQueue 内部用 mutex 保证线程安全,用两个条件变量实现生产者和消费者的协调。使用者不需要关心锁和条件变量,只管 Push 和 Pop。


三、POSIX信号量

3.1 信号量是什么

信号量(Semaphore)是另一种同步工具,概念来自操作系统理论:

bash 复制代码
定义:
信号量是一个计数器,表示可用资源的数量

操作:
├─ P操作(wait/down):申请资源,计数器-1
│   如果计数器 < 0,线程阻塞
│
└─ V操作(post/up):释放资源,计数器+1
    如果有线程在等待,唤醒一个

初始值:
- 初始化为N,表示有N个可用资源

与互斥锁的区别:

bash 复制代码
mutex(互斥锁):
- 本质是二元信号量(值只能是0或1)
- 用于保护临界资源
- 必须由同一线程加锁和解锁

semaphore(信号量):
- 计数器,可以是任意非负整数
- 用于控制资源数量
- 可以由不同线程P和V

3.2 POSIX信号量API

c 复制代码
#include <semaphore.h>

// 初始化信号量
int sem_init(sem_t *sem, int pshared, unsigned int value);
参数:
  sem:信号量指针
  pshared:0表示线程间共享,非0表示进程间共享
  value:初始值
返回值:成功返回0,失败返回-1

// P操作:申请资源(可能阻塞)
int sem_wait(sem_t *sem);
// 如果信号量>0,则-1并返回
// 如果信号量=0,则阻塞等待

// V操作:释放资源
int sem_post(sem_t *sem);
// 信号量+1
// 如果有等待线程,唤醒一个

// 销毁信号量
int sem_destroy(sem_t *sem);

3.3 信号量 vs 条件变量

bash 复制代码
相同点:
- 都用于线程同步
- 都可以让线程等待和被唤醒

不同点:
┌────────────┬──────────────┬──────────────┐
│ 特性       │ 条件变量     │ 信号量       │
├────────────┼──────────────┼──────────────┤
│ 计数       │ 无计数       │ 有计数       │
│ 资源控制   │ 不直接控制   │ 直接控制数量 │
│ 配合使用   │ 必须配合mutex│ 可单独使用   │
│ 唤醒       │ signal/broadcast | post   │
│ 等待       │ wait         │ wait         │
│ 应用场景   │ 条件同步     │ 资源计数     │
└────────────┴──────────────┴──────────────┘

选择建议:
- 需要等待某个条件成立:用条件变量
- 需要控制资源数量:用信号量
- 生产者消费者:两者都可以

3.4 信号量的封装

Sem.hpp:

cpp 复制代码
#pragma once
#include <semaphore.h>

class Sem
{
public:
    Sem(const Sem &) = delete;
    const Sem &operator=(const Sem &) = delete;
    
    Sem(int value = 0)
    {
        sem_init(&_sem, 0, value);
    }
    
    void Wait()
    {
        sem_wait(&_sem);
    }
    
    void Post()
    {
        sem_post(&_sem);
    }
    
    ~Sem()
    {
        sem_destroy(&_sem);
    }
    
private:
    sem_t _sem;
};

四、环形队列实现

4.1 环形队列的设计

环形队列(Ring Buffer)相比普通队列,有几个优点:

bash 复制代码
优点1:固定大小,不需要动态分配内存
  - 普通队列:push时可能需要扩容
  - 环形队列:数组固定,循环使用

优点2:访问效率高
  - 数组随机访问 O(1)
  - 不需要频繁的内存分配释放

优点3:适合高性能场景
  - 无锁队列的基础
  - 实时系统常用

环形队列结构:

bash 复制代码
数组:[0] [1] [2] [3] [4]
      
生产者索引(p_pos):下一个要写入的位置
消费者索引(c_pos):下一个要读取的位置

初始状态:
p_pos = 0, c_pos = 0
[_] [_] [_] [_] [_]
 ↑
 p,c

生产1个数据:
p_pos = 1, c_pos = 0
[A] [_] [_] [_] [_]
     ↑   ←───────┘
     p           c

再生产2个:
p_pos = 3, c_pos = 0
[A] [B] [C] [_] [_]
             ↑   ←───────┘
             p           c

消费1个:
p_pos = 3, c_pos = 1
[A] [B] [C] [_] [_]
     ↑       ↑
     c       p

继续生产到满:
p_pos = 0, c_pos = 1
[E] [B] [C] [D] [E]
 ↑   ↑
 p   c

4.2 信号量控制

环形队列需要控制两个资源:

bash 复制代码
资源1:空格子(blank)
  - 初始值:capacity(队列容量)
  - 生产者需要空格子才能生产
  - 生产前 P(blank),生产后 V(data)

资源2:数据格子(data)
  - 初始值:0(开始没有数据)
  - 消费者需要数据格子才能消费
  - 消费前 P(data),消费后 V(blank)

流程:
生产者:
  P(blank)  ← 等待空格子
  放入数据
  V(data)   ← 增加数据计数

消费者:
  P(data)   ← 等待数据
  取出数据
  V(blank)  ← 增加空格子计数

4.3 为什么还需要锁

有了信号量控制资源,为什么还要锁?

bash 复制代码
场景:多生产者或多消费者

问题:
假设有2个生产者 P1, P2
1. P1 执行 P(blank),拿到空格子,准备写 pos=3
2. P2 执行 P(blank),拿到空格子,也准备写 pos=3
3. P1 和 P2 同时写 pos=3 → 数据竞争!

原因:
信号量只保证"有空格子"
不保证"哪个格子是我的"

解决:
用锁保护生产者索引的更新
同样,消费者索引也需要锁保护

4.4 完整实现

RingQueue.hpp:

cpp 复制代码
#pragma once
#include <iostream>
#include <vector>
#include <pthread.h>
#include "Sem.hpp"

const int default_capacity = 5;

template <typename T>
class RingQueue
{
public:
    RingQueue(int capacity = default_capacity)
        : _queue(capacity)
        , _capacity(capacity)
        , _sem_blank(capacity)  // 初始有capacity个空格子
        , _sem_data(0)          // 初始没有数据
        , _p_pos(0)
        , _c_pos(0)
    {
        pthread_mutex_init(&_p_mutex, nullptr);
        pthread_mutex_init(&_c_mutex, nullptr);
    }

    // 生产者调用
    void Push(const T &data)
    {
        // 1. 申请空格子(可能阻塞)
        _sem_blank.Wait();
        
        // 2. 拿到空格子,准备写入
        //    多个生产者要互斥访问 p_pos
        pthread_mutex_lock(&_p_mutex);
        _queue[_p_pos] = data;
        _p_pos = (_p_pos + 1) % _capacity;  // 环形前进
        pthread_mutex_unlock(&_p_mutex);
        
        // 3. 增加数据计数
        _sem_data.Post();
    }

    // 消费者调用
    void Pop(T *data)
    {
        // 1. 申请数据格子(可能阻塞)
        _sem_data.Wait();
        
        // 2. 拿到数据,准备读取
        //    多个消费者要互斥访问 c_pos
        pthread_mutex_lock(&_c_mutex);
        *data = _queue[_c_pos];
        _c_pos = (_c_pos + 1) % _capacity;  // 环形前进
        pthread_mutex_unlock(&_c_mutex);
        
        // 3. 增加空格子计数
        _sem_blank.Post();
    }

    ~RingQueue()
    {
        pthread_mutex_destroy(&_p_mutex);
        pthread_mutex_destroy(&_c_mutex);
    }

private:
    std::vector<T> _queue;   // 环形队列
    int _capacity;           // 容量
    
    Sem _sem_blank;          // 空格子信号量
    Sem _sem_data;           // 数据格子信号量
    
    int _p_pos;              // 生产者位置
    int _c_pos;              // 消费者位置
    
    pthread_mutex_t _p_mutex;  // 生产者互斥锁
    pthread_mutex_t _c_mutex;  // 消费者互斥锁
};

4.5 测试代码

cpp 复制代码
#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include "RingQueue.hpp"

RingQueue<int> rq;

void *producer(void *arg)
{
    int id = *(int *)arg;
    while (1) {
        int data = rand() % 100;
        rq.Push(data);
        std::cout << "生产者" << id << ": 生产数据 " << data << std::endl;
        sleep(1);
    }
    return nullptr;
}

void *consumer(void *arg)
{
    int id = *(int *)arg;
    while (1) {
        int data;
        rq.Pop(&data);
        std::cout << "消费者" << id << ": 消费数据 " << data << std::endl;
        sleep(2);
    }
    return nullptr;
}

int main()
{
    srand(time(nullptr));
    
    pthread_t p1, p2, c1, c2;
    int pid1 = 1, pid2 = 2;
    int cid1 = 1, cid2 = 2;
    
    pthread_create(&p1, nullptr, producer, &pid1);
    pthread_create(&p2, nullptr, producer, &pid2);
    pthread_create(&c1, nullptr, consumer, &cid1);
    pthread_create(&c2, nullptr, consumer, &cid2);
    
    pthread_join(p1, nullptr);
    pthread_join(p2, nullptr);
    pthread_join(c1, nullptr);
    pthread_join(c2, nullptr);
    
    return 0;
}

编译运行:

bash 复制代码
$ g++ test_ring_queue.cpp -o test -std=c++11 -lpthread
$ ./test
生产者1: 生产数据 83
生产者2: 生产数据 86
消费者1: 消费数据 83
生产者1: 生产数据 77
消费者2: 消费数据 86
生产者2: 生产数据 15
生产者1: 生产数据 93
消费者1: 消费数据 77
生产者2: 生产数据 35
消费者2: 消费数据 15
...

五、两种实现的对比

5.1 实现对比

bash 复制代码
┌────────────┬──────────────────┬──────────────────┐
│ 特性       │ BlockingQueue    │ RingQueue        │
├────────────┼──────────────────┼──────────────────┤
│ 底层容器   │ std::queue       │ vector(数组)     │
│ 容量语义   │ 有界(逻辑限制) │ 有界(物理限制)      │
│ 内存分配   │ 可能动态分配     │ 一次性分配           │
│ 同步工具   │ mutex + 2个cond  │ 2个sem + 2个mutex  │
│ 并发度     │ 生产/消费互斥    │ 生产/消费可并行       │
│ 实现复杂度 │ 简单             │ 稍复杂             │
└────────────┴──────────────────┴──────────────────┘
⚠️ 重要区分:模型级并行 vs 实现级并行

需要注意的是,生产者消费者模型提升整体效率的根本原因,
并不是"入队和出队是否并行",
而是"任务获取"和"任务处理"可以并行进行。

也就是说:
- 生产者在把任务放入队列后,可以立刻继续生产
- 消费者在取出任务后,可以独立执行耗时任务
二者互不阻塞。

即使在 BlockingQueue 中,
入队和出队操作本身需要通过同一把 mutex 串行执行,
这种模型级的并行仍然存在,
并且已经能显著提高系统吞吐量。

RingQueue 通过拆分生产者锁和消费者锁,
进一步减少了队列操作阶段的锁竞争,
属于实现层面的性能优化,
主要在高并发场景下才会体现明显优势。

六、一个完整的生产消费案例

6.1 任务类定义

模拟一个计算任务:

cpp 复制代码
class Task
{
public:
    Task() : _x(0), _y(0), _op('+') {}
    
    Task(int x, int y, char op)
        : _x(x), _y(y), _op(op)
    {}
    
    void Execute()
    {
        int result = 0;
        switch (_op) {
            case '+':
                result = _x + _y;
                break;
            case '-':
                result = _x - _y;
                break;
            case '*':
                result = _x * _y;
                break;
            case '/':
                result = (_y == 0) ? -1 : _x / _y;
                break;
            case '%':
                result = (_y == 0) ? -1 : _x % _y;
                break;
            default:
                std::cout << "未知操作" << std::endl;
                return;
        }
        std::cout << "处理任务: " << _x << " " << _op << " " 
                  << _y << " = " << result 
                  << " [线程:" << pthread_self() << "]" << std::endl;
    }
    
private:
    int _x;
    int _y;
    char _op;
};

6.2 完整测试

cpp 复制代码
#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include "BlockingQueue.hpp"
#include "Task.hpp"

BlockingQueue<Task> task_queue;

void *producer(void *arg)
{
    const char ops[] = "+-*/%";
    int id = *(int *)arg;
    
    while (1) {
        // 随机生成任务
        int x = rand() % 100;
        int y = rand() % 100;
        char op = ops[rand() % 5];
        
        Task task(x, y, op);
        task_queue.Push(task);
        
        std::cout << "生产者" << id << ": 生成任务 " 
                  << x << " " << op << " " << y << std::endl;
        
        sleep(1);
    }
    return nullptr;
}

void *consumer(void *arg)
{
    int id = *(int *)arg;
    while (1) {
        Task task;
        task_queue.Pop(&task);
        
        std::cout << "消费者" << id << ": 获取任务" << std::endl;
        task.Execute();
        
        usleep(500000);  // 0.5秒
    }
    return nullptr;
}

int main()
{
    srand(time(nullptr));
    
    pthread_t p1, p2;
    pthread_t c1, c2, c3;
    
    int pid1 = 1, pid2 = 2;
    int cid1 = 1, cid2 = 2, cid3 = 3;
    
    pthread_create(&p1, nullptr, producer, &pid1);
    pthread_create(&p2, nullptr, producer, &pid2);
    pthread_create(&c1, nullptr, consumer, &cid1);
    pthread_create(&c2, nullptr, consumer, &cid2);
    pthread_create(&c3, nullptr, consumer, &cid3);
    
    pthread_join(p1, nullptr);
    pthread_join(p2, nullptr);
    pthread_join(c1, nullptr);
    pthread_join(c2, nullptr);
    pthread_join(c3, nullptr);
    
    return 0;
}

运行结果(部分):

bash 复制代码
生产者1: 生成任务 83 + 86
消费者1: 获取任务
处理任务: 83 + 86 = 169 [线程:140234567]
生产者2: 生成任务 77 * 15
消费者2: 获取任务
处理任务: 77 * 15 = 1155 [线程:140234568]
生产者1: 生成任务 93 / 35
消费者3: 获取任务
处理任务: 93 / 35 = 2 [线程:140234569]
...

七、本篇总结

7.1 核心知识点

1. 生产者消费者模型

bash 复制代码
3要素:生产者、消费者、容器
2关系:互斥访问容器
1核心:缓冲区解耦

优点:
├─ 解耦:生产和消费独立
├─ 并发:充分利用多核
└─ 削峰:平衡速度差异

2. BlockingQueue实现

bash 复制代码
工具:mutex + 2个条件变量
  - cond_producer:生产者等待
  - cond_consumer:消费者等待

关键:
  - 队列满,生产者wait(cond_producer)
  - 队列空,消费者wait(cond_consumer)
  - 生产后signal(cond_consumer)
  - 消费后signal(cond_producer)

3. POSIX信号量

bash 复制代码
定义:计数器,表示资源数量
操作:
  - wait (P操作):申请资源,计数-1
  - post (V操作):释放资源,计数+1

vs 条件变量:
  - 信号量有计数,直接控制资源
  - 条件变量无计数,等待条件成立

4. RingQueue实现

bash 复制代码
工具:2个信号量 + 2个mutex
  - sem_blank:空格子计数
  - sem_data:数据格子计数
  - p_mutex:保护生产者索引
  - c_mutex:保护消费者索引

优势:
  - 生产和消费可以并行
  - 性能更高

7.2 两种实现选择

bash 复制代码
选择BlockingQueue:
  - 代码简单,易于理解
  - 通用场景,性能要求不高
  - 队列大小可变

选择RingQueue:
  - 高性能场景
  - 队列大小固定
  - 需要充分利用多核

7.3 最重要的理解

📌 生产者消费者模型的精髓

bash 复制代码
1. 容器是核心
   - 解耦生产和消费
   - 缓冲速度差异
   - 支持多对多

2. 需要两层保护
   - 互斥:保护容器数据
   - 同步:协调等待和唤醒

3. 两种实现各有优势
   - BlockingQueue:简单通用
   - RingQueue:高性能

4. 实际应用广泛
   - 线程池任务队列
   - 消息队列
   - 日志系统
   - 网络I/O缓冲

💬 下篇预告 :生产者消费者模型掌握后,我们就可以实现一个真正有用的东西------线程池(ThreadPool)。线程池是生产者消费者模型的典型应用:用户提交任务(生产者),工作线程处理任务(消费者),任务队列作为中间容器。下篇会从线程池原理讲起,设计一个日志系统(顺便学习策略模式),然后实现完整的线程池,最后用单例模式让线程池全局可用。

👍 点赞、收藏与分享:如果这篇文章对你有帮助,请点赞、收藏并分享!

相关推荐
遇见火星2 小时前
Linux Screen 命令入门指南
linux·运维·服务器
Queenie_Charlie2 小时前
八皇后问题
c++·深度优先搜索
m0_736919102 小时前
编译器命令选项优化
开发语言·c++·算法
Jiu-yuan2 小时前
C++函数
c++
naruto_lnq2 小时前
C++中的工厂方法模式
开发语言·c++·算法
一切尽在,你来2 小时前
C++多线程教程-1.2.3 C++并发编程的平台无关性
开发语言·c++
Doro再努力2 小时前
【Linux操作系统06】深入理解权限掩码与粘滞位
linux·运维·服务器
wdfk_prog2 小时前
[Linux]学习笔记系列 -- [drivers][dma]stm32-dma
linux·笔记·学习
mzhan0172 小时前
[Linux] vdso 32bit vs 64bit
linux·运维·服务器