跳到主要内容
极客日志极客日志面向AI+效率的开发者社区
首页博客我的书AI学习GitHub 精选镜像AI 生图工具UI配色美学关于
搜索内容 / 工具 / 仓库 / 镜像...⌘K搜索
注册
博客列表
C++算法

Linux 多线程:生产者消费者模型、阻塞队列与条件变量详解

详细解析 Linux 多线程中的生产者消费者模型,通过超市类比解释解耦原理。重点阐述了线程间的互斥与同步关系,包括消费者竞争、生产者竞争及产消同步。提供了基于 pthread 和 C++ 模板的阻塞队列完整实现代码,涵盖 Task 类定义与 BlockQueue 核心逻辑。深入探讨了判断条件为何需先加锁的原因,以及伪唤醒现象的产生机制与解决方案,强调使用 while 循环而非 if 判断来防止条件变量误唤醒,确保高并发场景下的线程安全。

草莓泡芙发布于 2026/2/4更新于 2026/9/125.8K 浏览
Linux 多线程:生产者消费者模型、阻塞队列与条件变量详解

什么是生产者消费者模型

生产者消费者模式通过一个容器来解决生产者和消费者的强耦合问题。生产者和消费者彼此之间不直接通信,而是通过阻塞队列进行通信。生产者生产完数据后不等待消费者处理,直接放入阻塞队列;消费者不从生产者处获取数据,而是从阻塞队列中取。阻塞队列相当于一个缓冲区,平衡了消费者和生产者的处理能力,实现了二者的解耦。

文章配图

超市的现实例子

生活中买东西通常去超市而不是直接找供货商。假如需要买一桶方便面,直接找供货商可能不会成功,因为工厂生产是批量进行的,单件购买会导致成本过高且库存积压。现实生活中,供货商通过超市进行生产和消费的解耦。消费者(人)不需要直接向生产者(供货商)要数据,只需从超市(缓冲区)拿取即可。这样既平衡了处理能力,又避免了直接耦合带来的效率问题。

线程视角下的生产者消费者模型

生产者消费者模式本质上是线程间如何安全高效地进行通信。生产者负责生产数据的线程,消费者负责处理数据的线程,而超市则是一段具有特定结构的内存空间。由于生产者和消费者的数据通过这段共享内存空间通信,因此会产生各种并发问题:

  1. 消费者 VS 消费者:互斥。当资源充足时竞争不明显,但当资源稀缺(如只剩一桶方便面)时,消费者之间存在激烈的竞争关系,属于互斥。
  2. 生产者 VS 生产者:互斥。多个生产者同时向队列写入数据,存在竞争关系,需要互斥保护。
  3. 生产者 VS 消费者:互斥与同步。互斥体现在记录人员记录货物时消费者不可拿走;同步体现在队列为空时消费者需等待生产者供货,队列已满时生产者需等待消费者消费。

main 函数

int main() {
    // 设置随机种子
    srand(time(nullptr));
    // 创建阻塞队列
    BlockQueue<Task> *bq = new BlockQueue<Task>;
    pthread_t c[3], p[5];
    
    // 创建多生产者线程
    for (int i = 0; i < 5; i++) {
        pthread_create(p + i, nullptr, Producer, bq);
    }
    
    // 创建多消费者线程
    for (int i = 0; i < 3; i++) {
        pthread_create(c + i, nullptr, Consumer, bq);
    }
    
    // 等待线程结束
    for (int i = 0; i < 5; i++) {
        pthread_join(p[i], nullptr);
    }
    for (int i = 0; i < 3; i++) {
        pthread_join(c[i], nullptr);
    }
    return 0;
}

文章配图

从结果来看,一个简单的生产者消费者模型创建出来了。但代码中存在一个关于条件变量使用的常见误区,可能导致程序出现伪唤醒的情况。

生产者线程函数

void *Producer(void *args) {
    BlockQueue<Task> *bq = (BlockQueue<Task> *)args;
    std::string oper("+-*/%");
    while (1) {
        int x = rand() % 10;
        int y = rand() % 10;
        Task task(x, y, oper[rand() % 5]);
        
        // 向阻塞队列中放入任务
        // 如果队列已满,会在 push() 内部阻塞
        bq->push(task);
        std::cout << "生产了一个任务 : ";
        task.getTask();
        sleep(1);
    }
}

消费者线程函数

void *Consumer(void *args) {
    BlockQueue<Task> *bq = (BlockQueue<Task> *)args;
    while (1) {
        // 从阻塞队列中取任务
        // 如果队列为空,会在 pop() 内部阻塞
        Task task = bq->pop();
        std::cout << "消耗了一个任务 : ";
        task.run();
        sleep(2);
    }
}

Task 类定义

class Task {
public:
    Task(int x, int y, char oper, int result = 0, int exitcode = 0)
        : x_(x), y_(y), oper_(oper), result_(result), exitcode_(exitcode) {}

    void run() {
        switch (oper_) {
            case '+': result_ = x_ + y_; break;
            case '-': result_ = x_ - y_; break;
            case '*': result_ = x_ * y_; break;
            case '/':
                if (y_ == 0) { exitcode_ = 1; }
                else { result_ = x_ / y_; }
                break;
            case '%':
                if (y_ == 0) { exitcode_ = 2; }
                else { result_ = x_ % y_; }
                break;
        }
        printf("%d %c %d = %d[%d]\n", x_, oper_, y_, result_, exitcode_);
    }

    void getTask() {
        printf("%d %c %d = ?\n", x_, oper_, y_);
    }

private:
    int x_;
    int y_;
    char oper_;
    int result_;
    int exitcode_;
};

BlockQueue 阻塞队列实现

template <class T>
class BlockQueue {
public:
    BlockQueue(int bqmax = 5) : bqmax_(bqmax) {
        pthread_mutex_init(&mutex_, nullptr);
        pthread_cond_init(&c_cond_, nullptr);
        pthread_cond_init(&p_cond_, nullptr);
    }

    T pop() {
        pthread_mutex_lock(&mutex_);
        if (bq_.size() == 0) {
            pthread_cond_wait(&c_cond_, &mutex_);
        }
        T top = bq_.front();
        bq_.pop();
        pthread_cond_signal(&p_cond_);
        pthread_mutex_unlock(&mutex_);
        return top;
    }

    void push(const T& in) {
        pthread_mutex_lock(&mutex_);
        if (bq_.size() == bqmax_) {
            pthread_cond_wait(&p_cond_, &mutex_);
        }
        bq_.push(in);
        pthread_cond_signal(&c_cond_);
        pthread_mutex_unlock(&mutex_);
    }

    ~BlockQueue() {
        pthread_mutex_destroy(&mutex_);
        pthread_cond_destroy(&c_cond_);
        pthread_cond_destroy(&p_cond_);
    }

private:
    std::queue<T> bq_;
    int bqmax_;
    pthread_mutex_t mutex_;
    pthread_cond_t c_cond_;
    pthread_cond_t p_cond_;
};

为什么判断条件要先加锁?

文章配图

void *getTicket(void *args) {
    threadDate *td = (threadDate *)args;
    while (1) {
        pthread_mutex_lock(td->mutex_);
        if (tickets > 0) {
            usleep(1000);
            printf("%s get a tickets , tickets : %d\n", td->threadname.c_str(), tickets);
            tickets--;
            pthread_mutex_unlock(td->mutex_);
        } else {
            pthread_mutex_unlock(td->mutex_);
            break;
        }
    }
    return nullptr;
}

阻塞队列有两个典型约束:

  • 队列满时:生产者不能继续生产
  • 队列空时:消费者不能继续消费

这是资源暂时不满足条件的情况。为了防止多消费者拿到同一个数据或多生产者造成数据混乱,在多线程操作时必须加锁。同时,当队列满或为空时需进行判断,若满足条件则阻塞。判断临界资源是否满足条件本身也是在访问临界资源,若不先加锁,多个线程可能同时进入判断,导致数据异常(如票数变负)。因此必须先加锁再判断。

伪唤醒问题及解决方案

当资源不满足时,当前线程挂起阻塞,直到资源就绪。持有锁的线程挂起时会释放锁,以便其他线程申请。条件变量的第二个参数即为互斥锁,用于在线程挂起时释放锁,唤醒后重新申请。

在多生产者多消费者场景下,假设队列已满,一个消费线程消费后调用 pthread_cond_broadcast 唤醒了多个生产者。其中一个获得锁并填充空位后,队列再次满。此时其他被唤醒的生产者若获得锁,可能会误以为资源可用而继续生产,导致数据溢出,这就是伪唤醒。

为避免伪唤醒,应使用循环判断条件是否满足:

T pop() {
    pthread_mutex_lock(&mutex_);
    while (bq_.size() == 0) {
        pthread_cond_wait(&c_cond_, &mutex_);
    }
    T top = bq_.front();
    bq_.pop();
    pthread_cond_signal(&p_cond_);
    pthread_mutex_unlock(&mutex_);
    return top;
}

void push(const T& in) {
    pthread_mutex_lock(&mutex_);
    while (bq_.size() == bqmax_) {
        pthread_cond_wait(&p_cond_, &mutex_);
    }
    bq_.push(in);
    pthread_cond_signal(&c_cond_);
    pthread_mutex_unlock(&mutex_);
}

即使生产者申请到锁,再次判断时若资源依旧不满足,条件变量会将其挂起并释放锁,从而避免伪唤醒。

生产者消费者模型是多线程编程中最基础也是最重要的模式。通过阻塞队列,生产者和消费者可以安全、高效地协作,同时避免资源竞争和伪唤醒问题。理解了互斥、同步和条件变量的配合,就能轻松应对线程安全设计和高并发场景。

目录

  1. 什么是生产者消费者模型
  2. 超市的现实例子
  3. 线程视角下的生产者消费者模型
  4. main 函数
  5. 生产者线程函数
  6. 消费者线程函数
  7. Task 类定义
  8. BlockQueue 阻塞队列实现
  9. 为什么判断条件要先加锁?
  10. 伪唤醒问题及解决方案

更多推荐文章

查看全部
  • MiniMax 海螺 AI:图片与文本生成高质量视频实战
  • Clawdbot 飞书机器人集成配置实战指南
  • 如何将本地 LLM 模型与 Ollama 和 Python 集成
  • pg_lake 核心功能:Parquet/CSV/JSON 文件查询与导入技巧
  • 什么样的人不适合学编程:行业现状与能力评估
  • 苹果 MATLAB MAC 版安装教程
  • 2026 年 Web 前端开发八大趋势解析
  • 网络安全常见十大漏洞总结:原理、危害与防御策略
  • Stable Diffusion 模型技术深度解析
  • Qwen3-VL 模型架构及原理详解
  • Agent / RAG / MCP / Skill 通用工程解法与技术选型
  • Spatial Joy 2025 全球 AR&AI 开发大赛参赛指南与资源介绍
  • 2025 团体程序设计天梯赛 L1-L2 题解(C++)
  • 人工智能大模型部署与工程化落地实战
  • OpenClaw 龙虾机器人本地部署与配置教程
  • Flutter 三方库 groq_sdk 的鸿蒙化适配指南
  • C++ STL list 容器底层实现详解
  • Python 基于随机森林的心脏衰竭风险预测系统开发
  • MySQL 监控实战:mysql_exporter 部署与远程监控
  • Java 算法面试基础:语法、容器与工具类总结

相关免费在线工具

  • 加密/解密文本

    使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online

  • Gemini 图片去水印

    基于开源反向 Alpha 混合算法去除 Gemini/Nano Banana 图片水印,支持批量处理与下载。 在线工具,Gemini 图片去水印在线工具,online

  • Base64 字符串编码/解码

    将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online

  • Base64 文件转换器

    将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online

  • Markdown转HTML

    将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online

  • HTML转Markdown

    将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online