目录
[双条件变量 vs 单条件变量](#双条件变量 vs 单条件变量)
[禁止拷贝 - 避免pthread句柄](#禁止拷贝 - 避免pthread句柄)
1.生产者消费者模型

概念
生产者消费者模型是一种并发设计模式 ,通过引入一个线程安全的有界缓冲区(通常是阻塞队列),利用互斥锁或者信号量保证临界区安全(互斥),利用条件变量或者信号量协调生产与消费的节奏(同步)。它实现了生产与消费的解耦,将串行的依赖关系转化为并行的流水线作业
模型结构
- 3种关系
| 关系 | 注意事项 | 应对机制 |
|---|---|---|
| 生产者 vs 生产者 | 不能同时向临界资源(队列)写入数据 | 互斥 |
| 消费者 vs 消费者 | 不能同时从临界资源取出数据 | 互斥 |
| 生产者 vs 消费者 | 互斥:不能同时访问队列 同步:队列空时消费者等待,队列满时生产者等待 | 互斥 + 同步 |
互斥由锁/二元信号量保证;同步由条件变量/信号量计数实现
- 2种角色
生产者 :获取/生成数据 → 放入队列
消费者:从队列取数据 → 加工处理
- 1个交易场所
特定结构的内存空间(通常是线程安全的阻塞队列),本质是缓冲区
-
数据流向
数据源 → 生产者 → [有界缓冲区] → 消费者 → 结果/加工
工作机制
- 生产者行为
获取/生成数据
若队列未满:放入数据,唤醒可能等待的消费者
若队列已满:阻塞/等待
- 消费者行为
从队列取数据
若队列非空:取出数据,唤醒可能等待的生产者
若队列为空:阻塞/等待
- 并发执行
生产者在获取/生成数据 与 消费者在加工处理数据 这两段可以完全并发
真正互斥的只是对队列的操作瞬间
模型优点
| 优点 | 解释 |
|---|---|
| 解耦 | 生产者和消费者不直接通信,只依赖队列,降低耦合度 |
| 支持并发 | 生产和消费过程可以重叠执行,提高整体吞吐量 |
| 支持忙闲不均 | 队列作为缓冲区,可以平衡生产速度与消费速度的差异 |
| 提高响应性 | 生产者不需等待消费者处理完,可立即处理下一任务 |
高效原因(并发本质)
生产者消费者模型的高效,不是因为减少了操作总量,而是将串行依赖变成并行重叠
如果不使用该模型:生产者生产完 → 等待消费者处理完 → 再生产(串行)
使用模型后:生产者放入队列即可继续生产,消费者从队列取数据同时处理
获取/生成数据 与加工/处理数据 是两个独立的处理阶段,可以在不同线程/进程中同时执行
这就是并发执行数据处理不同阶段带来的效率提升
2.基于阻塞队列的生产者消费者模型
代码实现
#pragma once
#include <queue>
#include <pthread.h>
#include <utility> // for std::move
/*
* 阻塞队列 - 线程安全的生产者消费者缓冲区
* 核心:互斥锁保护临界区 + 双条件变量实现同步 + 水位线批量唤醒
*/
template<class T>
class BlockQueue {
static const int defaultnum = 20;
public:
/*
* 构造函数
* @param num: 队列最大容量
* 初始化:锁、条件变量、水位线、关机标志
*/
BlockQueue(int num = defaultnum)
: maxCap_(num)
, high_water_(maxCap_ * 2 / 3) // 高水位线:2/3容量
, lower_water_(maxCap_ / 3) // 低水位线:1/3容量
, is_shutdown_(false)
{
pthread_mutex_init(&lock_, nullptr);
pthread_cond_init(&c_cond_, nullptr); // 消费者等待队列不空
pthread_cond_init(&p_cond_, nullptr); // 生产者等待队列不满
}
// 禁止拷贝/移动 - 包含pthread句柄的对象不能浅拷贝
BlockQueue(const BlockQueue&) = delete;
BlockQueue& operator=(const BlockQueue&) = delete;
BlockQueue(BlockQueue&&) = delete;
BlockQueue& operator=(BlockQueue&&) = delete;
/*
* 析构函数
* 警告:必须先调用shutdown(),确保无线程在等待
*/
~BlockQueue() {
pthread_mutex_destroy(&lock_);
pthread_cond_destroy(&p_cond_);
pthread_cond_destroy(&c_cond_);
}
/*
* 优雅关机
* 1. 设置关机标志
* 2. 广播唤醒所有等待线程
* 3. 线程检测到关机标志后主动退出
*/
void shutdown() {
pthread_mutex_lock(&lock_);
is_shutdown_ = true;
pthread_cond_broadcast(&p_cond_); // 唤醒所有生产者
pthread_cond_broadcast(&c_cond_); // 唤醒所有消费者
pthread_mutex_unlock(&lock_);
}
/*
* 生产者接口 - 左值版本
* 1. RAII锁守卫自动加解锁
* 2. while循环防止虚假唤醒
* 3. 关机时直接返回
* 4. 高水位批量唤醒消费者
*/
void push(const T& data) {
LockGuard guard(lock_);
// 必须用while:唤醒后需重新检查条件(虚假唤醒/多线程竞争)
while (q_.size() == maxCap_ && !is_shutdown_)
pthread_cond_wait(&p_cond_, &lock_);
if (is_shutdown_) return; // 关机后不再生产
q_.push(data);
// 达到高水位才唤醒消费者 - 减少上下文切换
if (q_.size() >= high_water_)
pthread_cond_signal(&c_cond_);
}
/*
* 生产者接口 - 右值版本(移动语义优化)
* 作用:大对象零拷贝,性能优化
*/
void push(T&& data) {
LockGuard guard(lock_);
while (q_.size() == maxCap_ && !is_shutdown_)
pthread_cond_wait(&p_cond_, &lock_);
if (is_shutdown_) return;
q_.push(std::move(data)); // 移动而非拷贝
if (q_.size() >= high_water_)
pthread_cond_signal(&c_cond_);
}
/*
* 消费者接口
* @param data: 输出参数,接收取出的数据
* @return: false-队列空且已关机;true-成功取出数据
* 1. bool返回值比直接返回T更健壮(错误处理显式化)
* 2. 低水位唤醒生产者
*/
bool pop(T& data) {
LockGuard guard(lock_);
while (q_.size() == 0 && !is_shutdown_)
pthread_cond_wait(&c_cond_, &lock_);
if (q_.empty() && is_shutdown_) return false; // 无数据可消费
data = std::move(q_.front()); // 移动语义
q_.pop();
// 低于低水位才唤醒生产者 - 批量生产
if (q_.size() <= lower_water_)
pthread_cond_signal(&p_cond_);
return true;
}
private:
/*
* RAII锁守卫
* 优点:异常安全、避免手动解锁遗漏
* 原理:构造加锁,析构解锁
*/
class LockGuard {
pthread_mutex_t& mutex_;
public:
LockGuard(pthread_mutex_t& mutex) : mutex_(mutex) {
pthread_mutex_lock(&mutex_);
}
~LockGuard() {
pthread_mutex_unlock(&mutex_);
}
};
private:
std::queue<T> q_; // 共享缓冲区(临界资源)
int maxCap_; // 容量上限
int high_water_; // 高水位线(唤醒消费者阈值)
int lower_water_; // 低水位线(唤醒生产者阈值)
bool is_shutdown_; // 优雅关机标志
pthread_mutex_t lock_; // 互斥锁:保护临界区
pthread_cond_t p_cond_; // 生产者条件变量:等待队列不满
pthread_cond_t c_cond_; // 消费者条件变量:等待队列不空
};
临界资源与锁的关系
| 核心原则 | 代码体现 | 知识点 |
|---|---|---|
| 判断即访问 | q_.size() == 0 |
判断临界资源是否就绪,本身就是访问临界资源 |
| 先加锁,后判断 | pthread_mutex_lock() → if(q_.size()==0) |
判断必须在锁保护下进行,否则竞态条件 |
| 判断不是一次性的 | while 循环 |
唤醒后必须重新判断,防止伪唤醒/竞争 |
判断临界资源的状态,本身就是访问临界资源,必须在加锁之后
防止虚假唤醒
while (条件不满足 && !is_shutdown_)
pthread_cond_wait(&cond, &lock);
while循环在每次唤醒后重新检查条件表达式,只有条件真正成立时才继续执行,否则再次进入等待,从根本上保证了无论唤醒原因为何,都不会在条件不满足时误操作临界资源
锁的粒度
{
LockGuard guard(lock_); // 临界区开始
q_.push(data); // 只保护共享资源的操作
pthread_cond_signal(...);
} // 临界区结束,立即解锁
临界区要足够小------只保护共享资源的访问,不保护业务逻辑
双条件变量 vs 单条件变量
| 方案 | 代码 | 问题 |
|---|---|---|
| 单条件变量 | pthread_cond_t cond; |
唤醒不精准 |
| 双条件变量 | p_cond_ + c_cond_ |
精准通知 |
水位线设计思想
// 不是每次push都唤醒,达到阈值才唤醒
if (q_.size() >= high_water_) pthread_cond_signal(&c_cond_);
if (q_.size() <= lower_water_) pthread_cond_signal(&p_cond_);
普通实现:生产一个唤醒一个(频繁上下文切换)
水位线实现:积攒一批唤醒一次(批量处理)
禁止拷贝 - 避免pthread句柄
包含
pthread_mutex_t、pthread_cond_t等内核句柄的对象必须禁止拷贝 ,因为这些句柄本质上是系统资源的引用(文件描述符/指针),浅拷贝 只是复制了内存数值,并未真正复制内核对象;当多个对象析构时会对同一内核资源重复调用destroy导致未定义行为 (崩溃、死锁或资源泄露)。正确做法 :显式=delete拷贝构造/赋值,并通过智能指针或移动语义(如需要)实现唯一所有权转移,从根本上杜绝隐式浅拷贝带来的灾难性后果
此外,shutdown()使线程主动退出LockGuard 锁获取即初始化,异常安全
push移动语义优化
3.基于环形队列的生产者消费者模型

环形队列的约束条件
| 约束 | 含义 | 代码体现 |
|---|---|---|
| 同一位置只能一人访问 | 生产者和消费者指向同一个下标时,必须互斥 | 空:生产者写;满:消费者读 |
| 生产者不能超过消费者一圈 | 生产者领先消费者最多 cap-1 个位置 |
p_index_ 不能追上 c_index_ 套圈 |
| 消费者不能超过生产者 | 消费者不能消费未生产的数据 | c_index_ 永远 ≤ p_index_(模意义下) |
生产者在前跑,消费者在后追,不能套圈,相遇时要互斥
信号量的双重角色
| 信号量 | 角色 | 初始值 | 作用 |
|---|---|---|---|
p_spacesem_ |
空间资源计数器 | cap |
生产者申请空间,消费者释放空间 |
c_datasem_ |
数据资源计数器 | 0 |
消费者申请数据,生产者释放数据 |
环形队列天然适合信号量,因为"空位个数"和"数据个数"本身就是计数语义
信号量解决的三个问题:(1)生产者-消费者互斥:不满不空时,生产消费可并发;满/空时,只有一个执行流进入
(2)生产-消费同步:空时消费者阻塞,满时生产者阻塞
(3)资源计数管理:自动维护可用空间/数据个数
关键:信号量不解决同类线程互斥
代码实现
#pragma once
#include <vector>
#include <semaphore.h>
#include <pthread.h>
#include <atomic>
#include <stdexcept>
/**
* @brief 环形队列 - 基于信号量的高性能生产者消费者模型
*
* @tparam T 元素类型,要求至少满足移动赋值(推荐 noexpect 移动构造/赋值)
*
* 核心特性:
* - 双信号量:空间信号量 + 数据信号量(实现生产消费同步)
* - 双互斥锁:生产者锁 + 消费者锁(同类线程互斥,异类线程并行)
* - 优雅关机:原子标志 + 信号量广播唤醒
* - 移动语义:支持右值引用,大对象零拷贝
*
* 线程安全:是(多生产者/多消费者)
* 异常安全:基本保证
*/
template<class T>
class RingQueue {
public:
/**
* @brief 构造函数
* @param cap 队列容量,必须大于0
* @throw std::invalid_argument 当 cap <= 0 时抛出
*/
explicit RingQueue(int cap = 10)
: q_(cap)
, cap_(cap)
, c_index_(0)
, p_index_(0)
, is_shutdown_(false) {
if (cap <= 0) {
throw std::invalid_argument("RingQueue capacity must be positive");
}
// 信号量初始化
// c_datasem_: 消费者等待的数据资源,初始为0
// p_spacesem_: 生产者等待的空间资源,初始为cap
if (sem_init(&c_datasem_, 0, 0) != 0 ||
sem_init(&p_spacesem_, 0, cap) != 0) {
throw std::runtime_error("sem_init failed");
}
// 互斥锁初始化(保护同类线程的竞争)
if (pthread_mutex_init(&c_lock_, nullptr) != 0 ||
pthread_mutex_init(&p_lock_, nullptr) != 0) {
sem_destroy(&c_datasem_);
sem_destroy(&p_spacesem_);
throw std::runtime_error("pthread_mutex_init failed");
}
}
/**
* @brief 禁止拷贝构造 - 包含不可复制的系统资源
*/
RingQueue(const RingQueue&) = delete;
/**
* @brief 禁止拷贝赋值
*/
RingQueue& operator=(const RingQueue&) = delete;
/**
* @brief 析构函数
* @note 必须先调用 shutdown(),否则阻塞线程会导致资源泄露或崩溃
*/
~RingQueue() {
shutdown(); // 1. 唤醒所有等待线程
// 2. 销毁系统资源
sem_destroy(&c_datasem_);
sem_destroy(&p_spacesem_);
pthread_mutex_destroy(&p_lock_);
pthread_mutex_destroy(&c_lock_);
}
/**
* @brief 优雅关闭队列
*
* 1. 设置关机标志(原子操作,保证多线程可见性)
* 2. 唤醒所有阻塞的生产者和消费者
*
* @note 调用后:
* - 生产者:push 直接返回(不生产)
* - 消费者:继续消费完队列中剩余数据,然后返回 false
*/
void shutdown() {
is_shutdown_.store(true);
// 唤醒所有等待线程(即使多次 post 也是安全的)
sem_post(&p_spacesem_); // 唤醒生产者
sem_post(&c_datasem_); // 唤醒消费者
}
/**
* @brief 生产数据(左值版本)
* @param data 要生产的元素(拷贝语义)
* @note 若队列已满,调用线程阻塞
*/
void push(const T& data) {
if (is_shutdown_.load()) return;
// 1. 申请空间资源(信号量 P 操作)
sem_wait(&p_spacesem_);
// 2. 再次检查关机状态(防止刚被唤醒时已关机)
if (is_shutdown_.load()) {
sem_post(&p_spacesem_); // 归还资源
return;
}
// 3. 生产者互斥锁(保护 p_index_ 和队列写操作)
pthread_mutex_lock(&p_lock_);
q_[p_index_] = data; // 拷贝赋值
p_index_ = (p_index_ + 1) % cap_; // 环形移动
pthread_mutex_unlock(&p_lock_);
// 4. 释放数据资源(信号量 V 操作)
sem_post(&c_datasem_);
}
/**
* @brief 生产数据(右值版本)
* @param data 要生产的元素(移动语义)
* @note 移动语义优化:大对象零拷贝,性能提升显著
*/
void push(T&& data) {
if (is_shutdown_.load()) return;
sem_wait(&p_spacesem_);
if (is_shutdown_.load()) {
sem_post(&p_spacesem_);
return;
}
pthread_mutex_lock(&p_lock_);
q_[p_index_] = std::move(data); // 移动赋值(O(1))
p_index_ = (p_index_ + 1) % cap_;
pthread_mutex_unlock(&p_lock_);
sem_post(&c_datasem_);
}
/**
* @brief 消费数据
* @param out 输出参数,接收取出的元素(移动语义)
* @return true - 成功取出数据
* @return false - 队列已关闭且无数据
*
* @note 队列空时调用线程阻塞
* @note 关机行为:处理完已入队数据后返回 false
*/
bool pop(T& out) {
// 1. 快速失败:已关机且无数据
if (is_shutdown_.load() && sem_trywait(&c_datasem_) != 0) {
return false;
}
// 2. 等待数据资源(信号量 P 操作)
sem_wait(&c_datasem_);
// 3. 关机检查:若已关机且尝试获取数据失败,则退出
if (is_shutdown_.load() && sem_trywait(&c_datasem_) != 0) {
return false;
}
// 4. 消费者互斥锁(保护 c_index_ 和队列读操作)
pthread_mutex_lock(&c_lock_);
out = std::move(q_[c_index_]); // 移动赋值(O(1))
c_index_ = (c_index_ + 1) % cap_; // 环形移动
pthread_mutex_unlock(&c_lock_);
// 5. 释放空间资源(信号量 V 操作)
sem_post(&p_spacesem_);
return true;
}
private:
std::vector<T> q_; // 环形缓冲区(连续内存,cache友好)
int cap_; // 缓冲区容量
int c_index_; // 消费者下标(被 c_lock_ 保护)
int p_index_; // 生产者下标(被 p_lock_ 保护)
sem_t c_datasem_; // 数据信号量:消费者等待的数据个数
sem_t p_spacesem_; // 空间信号量:生产者等待的空位个数
pthread_mutex_t c_lock_; // 消费者互斥锁:保护 c_index_
pthread_mutex_t p_lock_; // 生产者互斥锁:保护 p_index_
std::atomic<bool> is_shutdown_; // 关机标志(内存可见性保证)
};
先信号量后锁的三大优点
| 优点 | 说明 | 收益 |
|---|---|---|
| 技术正确 | 信号量本身就是原子的,不需要锁保护 | 避免死锁 |
| 减少锁持有时间 | 阻塞发生在锁外,不占用锁资源 | 提高并发度 |
| 降低串行比例 | 申请信号量、申请锁、生产操作可流水 | 吞吐量提升 |
持有锁等待资源,如果锁被死占,并发度为 0,系统退化为串行。
先资源后锁减少了整体申请资源的时间
把阻塞操作放在锁外面,让锁只保护真正的临界区
P/V操作的顺序
// 生产者(顺序不可颠倒!)
sem_wait(&p_spacesem_); // 1.先申请空间资源
/* 生产数据 */
sem_post(&c_datasem_); // 2.再释放数据资源
// 消费者(顺序不可颠倒!)
sem_wait(&c_datasem_); // 1.先申请数据资源
/* 消费数据 */
sem_post(&p_spacesem_); // 2.再释放空间资源
生产者先拿空间,再给数据 ;消费者先拿数据,再还空间
双锁
因为 p_index_和c_index_ 是两个不同的变量,没有数据竞争,所以生产者和消费者可以并发
因为 p_index_ 只有一个,多个生产者同时修改会错乱,所以生产者之间必须互斥
因为 c_index_ 只有一个,多个消费者同时修改会错乱,所以消费者之间必须互斥
pthread_mutex_t p_lock_; // 保护 p_index_(生产者下标)
pthread_mutex_t c_lock_; // 保护 c_index_(消费者下标)
| 锁 | 保护对象 | 解决关系 |
|---|---|---|
p_lock_ |
p_index_ + 生产者写操作 |
生产者-生产者互斥 |
c_lock_ |
c_index_ + 消费者读操作 |
消费者-消费者互斥 |
生产者和消费者操作不同的变量(p_index_ vs c_index_)
没有数据竞争 → 可以用两把独立的锁
效果:生产者线程和消费者线程可以真正并行执行
| 方案 | 并发度 | 代码复杂度 |
|---|---|---|
| 单锁 | 生产消费互斥 | 简单 |
| 双锁 | 生产消费并行 | 稍复杂 |
禁止拷贝
RingQueue(const RingQueue&) = delete;
RingQueue& operator=(const RingQueue&) = delete;
sem_t、pthread_mutex_t 是系统资源句柄,浅拷贝 → 两个对象持有同一内核对象,析构时重复销毁 → 崩溃
4.二者对比
阻塞队列强在弹性(动态扩容+水位线调控),环形队列强在性能(双锁并行+零拷贝+缓存友好)
