Linux 之 【多线程】(基于阻塞队列的生产者消费者模型、基于环形队列的生产者消费者模型)

目录

1.生产者消费者模型

概念

模型结构

工作机制

模型优点

高效原因(并发本质)

2.基于阻塞队列的生产者消费者模型

​编辑

代码实现

临界资源与锁的关系

防止虚假唤醒

锁的粒度

[双条件变量 vs 单条件变量](#双条件变量 vs 单条件变量)

水位线设计思想

[禁止拷贝 - 避免pthread句柄](#禁止拷贝 - 避免pthread句柄)

3.基于环形队列的生产者消费者模型

环形队列的约束条件

信号量的双重角色

代码实现

先信号量后锁的三大优点

P/V操作的顺序

双锁

禁止拷贝

4.二者对比


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_tpthread_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.二者对比

阻塞队列强在弹性(动态扩容+水位线调控),环形队列强在性能(双锁并行+零拷贝+缓存友好)

相关推荐
远方16091 小时前
114-Oracle Database 26ai在Oracle Linux 9上的OUI图形界面安装
linux·服务器·数据库·sql·oracle·database
开开心心_Every2 小时前
在线看报软件, 22家知名报刊免费看
linux·运维·服务器·华为od·edge·pdf·华为云
木子欢儿3 小时前
debian 13 安装配置ftp 创建用户admin可以访问 /mnt/Data/
linux·运维·服务器·数据库·debian
wsad05323 小时前
Xshell 连接 CentOS 7 Minimal 完整配置指南
linux·运维·centos
小程同学>o<3 小时前
Linux 应用层开发入门(二十三)| 异步通知方式读取输入数据
linux·嵌入式软件·地瓜机器人·atomgit·linux应用层开发·openloong开源社区·开源新春集福
czxyvX4 小时前
005-Linux基础开发工具
linux
Linux运维技术栈4 小时前
jumpserver堡垒机从 CentOS 7 迁移至 Rocky Linux 9 实战指南
linux·运维·服务器·centos·rocky
wsad05324 小时前
CentOS 7 Minimal 常用软件工具安装指南
linux·运维·centos
开开心心就好4 小时前
轻松加密文件生成exe,无需原程序解密
linux·运维·服务器·windows·pdf·harmonyos·1024程序员节