文章目录
-
- 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)。线程池是生产者消费者模型的典型应用:用户提交任务(生产者),工作线程处理任务(消费者),任务队列作为中间容器。下篇会从线程池原理讲起,设计一个日志系统(顺便学习策略模式),然后实现完整的线程池,最后用单例模式让线程池全局可用。
👍 点赞、收藏与分享:如果这篇文章对你有帮助,请点赞、收藏并分享!