news 2026/2/16 17:41:37

【C/C++】生产者消费者

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
【C/C++】生产者消费者

生产者-消费者模式详解

概述

生产者-消费者模式是多线程编程中最经典的同步模式之一。它描述了两类线程之间的协作关系:生产者负责生成数据并放入缓冲区,消费者从缓冲区取出数据进行处理。这个模式的核心挑战在于如何安全地协调这两类线程,避免竞态条件,同时保证高效的资源利用。

为什么需要有界队列

在实际应用中,我们通常使用有界队列作为缓冲区。这带来两个关键的同步点:当队列满时,生产者必须等待;当队列空时,消费者必须等待。这种双向的等待机制正是条件变量大显身手的地方。

完整实现

#include<atomic>#include<chrono>#include<condition_variable>#include<csignal>#include<cstdlib>#include<iostream>#include<mutex>#include<queue>#include<thread>#include<vector>// 互斥锁,保护共享队列的访问std::mutex mu;// 两个条件变量分别用于不同的等待条件// cv_prod: 生产者等待"队列未满"// cv_cons: 消费者等待"队列非空"std::condition_variable cv_prod,cv_cons;// 共享的有界队列std::queue<int>q;constexprintcap=10;// 原子变量,用于优雅退出// 使用 atomic 是因为信号处理函数和其他线程都会访问它std::atomic<bool>shutdown_flag{false};// 信号处理函数// 注意:信号处理函数中只能安全地使用 async-signal-safe 的操作// 写入 atomic<bool> 和 write() 系统调用是安全的voidsignal_handler(intsig){// 使用 write 而不是 cout,因为 cout 不是 async-signal-safeconstcharmsg[]="\nReceived Ctrl+C, shutting down...\n";write(STDOUT_FILENO,msg,sizeof(msg)-1);// 设置退出标志,使用 relaxed 内存序即可shutdown_flag.store(true,std::memory_order_relaxed);// 必须唤醒所有等待的线程,让它们检查 shutdown_flag// 这里用 notify_all 而不是 notify_onecv_prod.notify_all();cv_cons.notify_all();}voidproducer(){while(true){std::unique_lock<std::mutex>lk(mu);// 等待条件:队列未满 或者 收到退出信号// 必须把 shutdown_flag 加入条件,否则线程可能永远阻塞cv_prod.wait(lk,[]{returnq.size()<cap||shutdown_flag.load(std::memory_order_relaxed);});// 检查是否需要退出if(shutdown_flag.load(std::memory_order_relaxed)){std::cout<<std::this_thread::get_id()<<" Producer exiting"<<std::endl;return;}intx=rand();std::cout<<std::this_thread::get_id()<<" Producing "<<x<<std::endl;q.push(x);// 通知一个等待的消费者:队列非空了cv_cons.notify_one();}}voidconsumer(){while(true){std::unique_lock<std::mutex>lk(mu);// 等待条件:队列非空 或者 收到退出信号cv_cons.wait(lk,[]{return!q.empty()||shutdown_flag.load(std::memory_order_relaxed);});// 检查是否需要退出// 注意:即使收到退出信号,如果队列还有数据,可以选择继续消费// 这里选择直接退出,根据业务需求可以调整if(shutdown_flag.load(std::memory_order_relaxed)){std::cout<<std::this_thread::get_id()<<" Consumer exiting"<<std::endl;return;}intx=q.front();q.pop();std::cout<<std::this_thread::get_id()<<" Consuming "<<x<<std::endl;// 通知一个等待的生产者:队列有空位了cv_prod.notify_one();}}intmain(){// 注册信号处理函数std::signal(SIGINT,signal_handler);std::vector<std::thread>ts;intcnt=5;for(inti=0;i<cnt;i++){ts.emplace_back(producer);ts.emplace_back(consumer);}// 等待所有线程结束for(auto&t:ts){t.join();}std::cout<<"All threads finished, goodbye!"<<std::endl;return0;}

实现细节

为什么需要两个条件变量

代码中使用了两个条件变量:cv_prodcv_cons。这不仅仅是效率问题,更重要的是避免死锁。

考虑只用一个条件变量的情况:

std::condition_variable cv;voidproducer(){std::unique_lock<std::mutex>lk(mu);cv.wait(lk,[]{returnq.size()<cap;});q.push(x);cv.notify_one();// 想唤醒消费者}voidconsumer(){std::unique_lock<std::mutex>lk(mu);cv.wait(lk,[]{return!q.empty();});q.pop();cv.notify_one();// 想唤醒生产者}

假设队列已满,有多个生产者在等待。此时一个消费者取走一个元素,调用notify_one。问题来了:notify_one唤醒的是等待队列中的任意一个线程,可能是生产者,也可能是另一个消费者。如果唤醒的是消费者,而队列此时为空,这个消费者检查条件失败,又回去睡觉。而那些等待"队列未满"的生产者永远不会被唤醒。

更极端的情况:所有生产者都在等待队列未满,所有消费者都在等待队列非空,每次notify_one都唤醒了错误类型的线程,导致所有线程都在睡觉,程序死锁。

使用两个条件变量彻底解决这个问题。生产者只在cv_prod上等待,消费者只在cv_cons上等待。生产者放入元素后通知cv_cons,保证唤醒的一定是消费者;消费者取出元素后通知cv_prod,保证唤醒的一定是生产者。

wait 的工作原理

cv.wait(lk, predicate)这行代码背后隐藏了相当多的复杂性。它等价于:

while(!predicate()){cv.wait(lk);}

当谓词返回 false 时,wait 会原子性地释放锁并将当前线程放入条件变量的等待队列。这个原子性至关重要:如果先释放锁再进入等待队列,另一个线程可能在这个间隙中修改条件并发出通知,而我们会错过这个通知,导致永久阻塞。

当线程被唤醒时,它首先要重新获取互斥锁。如果锁被其他线程持有,它会在锁的等待队列上阻塞。获取锁之后,wait 返回,线程重新检查谓词。这个循环检查是必要的,因为存在虚假唤醒的可能,而且在多生产者或多消费者的场景下,可能有其他线程抢先处理了数据。

notify_one vs notify_all

正常运行时代码使用notify_one,因为每次只有一个元素被添加或移除,只需要唤醒一个等待的线程。使用notify_all会导致惊群效应:所有等待的线程被唤醒,但只有一个能成功操作,其余的检查条件失败后又回到等待状态,白白浪费了 CPU 时间。

但在信号处理函数中,我们必须使用notify_all。因为我们需要唤醒所有等待的线程让它们检查shutdown_flag并优雅退出。如果只用notify_one,可能只有一个线程被唤醒并退出,其他线程会永远阻塞。

内存序的选择

代码中对shutdown_flag的访问使用了memory_order_relaxed,而不是默认的memory_order_seq_cst。这是一个有意的选择。

对于这种简单的退出标志,我们只关心"最终能看到 true",而不关心"看到 true 的时候其他变量处于什么状态"。relaxed保证原子性和最终可见性,这就足够了。我们不需要shutdown_flag和其他内存操作之间有任何顺序关系。

而且条件变量的waitnotify_all内部已经包含了足够的同步原语。当线程被唤醒并重新获取锁之后,它一定能看到shutdown_flag的最新值。

使用seq_cst不会导致错误,只是白白付出性能代价。在 x86 架构上差别不大,因为 x86 本身是强内存序模型。但在 ARM 等弱内存序架构上,seq_cst需要插入额外的内存屏障指令,会有可观的性能开销。

什么时候需要更强的内存序?当你用原子变量来同步其他非原子数据的时候。比如经典的"发布-获取"模式:生产者先写入数据,再用release写入标志;消费者用acquire读取标志,再读取数据。这样可以保证消费者看到标志为 true 时,一定能看到完整的数据。但在我们的场景中,所有共享数据都由互斥锁保护,不需要原子变量来提供额外的同步。

优雅退出的实现

优雅退出需要解决几个问题:

第一,如何通知所有线程退出?我们使用std::atomic<bool>作为退出标志。原子类型保证了多线程环境下的安全访问,无需额外加锁。

第二,如何让阻塞在wait上的线程醒来?关键在于修改等待条件,把shutdown_flag加入谓词中。这样当shutdown_flag变为 true 时,谓词返回 true,线程不再阻塞。同时在信号处理函数中调用notify_all立即唤醒所有等待的线程。

第三,信号处理函数中能做什么?信号处理函数的执行环境很特殊,很多操作是不安全的。我们使用write系统调用而不是std::cout来输出信息,因为write是 async-signal-safe 的,而std::cout不是。写入std::atomic<bool>也是安全的。

关于锁和条件变量的销毁

C++ 标准规定,销毁一个还有线程在等待的条件变量是未定义行为。因此我们必须确保所有线程都已经退出等待状态并完成执行后,才能让条件变量被销毁。这就是为什么main函数中要join所有线程的原因之一。

总结

生产者-消费者模式看似简单,但正确实现需要对条件变量、互斥锁、原子操作和信号处理都有清晰的理解。关键点包括:使用两个条件变量避免死锁并精确唤醒正确类型的线程,理解 wait 的原子性释放锁和进入等待队列的机制,根据实际需求选择合适的内存序,以及优雅退出时需要唤醒所有等待线程。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/2/14 18:02:45

Holistic Tracking教育套件:学校机房也能用的云端AI实验室

Holistic Tracking教育套件&#xff1a;学校机房也能用的云端AI实验室 引言&#xff1a;当AI教育遇上老旧电脑 "老师&#xff0c;我们学校的电脑跑不动Stable Diffusion..."这是许多信息技术老师面临的现实困境。当GTX 750显卡遇上需要RTX 3060的AI应用&#xff0c…

作者头像 李华
网站建设 2026/2/14 13:05:20

3个最火动作捕捉模型推荐:MediaPipe Holistic开箱即用,5元全试遍

3个最火动作捕捉模型推荐&#xff1a;MediaPipe Holistic开箱即用&#xff0c;5元全试遍 引言 作为一名游戏公司的新人&#xff0c;突然被安排调研动作捕捉方案&#xff0c;面对MoveNet、OpenPose等专业名词是不是一头雾水&#xff1f;每个模型都要配置不同的环境&#xff0c…

作者头像 李华
网站建设 2026/2/7 1:12:55

机器人十年演进

下面我从工程、系统与产业前沿的角度&#xff0c;给你一条清晰的 「机器人十年演进路线&#xff08;2025–2035&#xff09;」。这不是“更像人”的畅想&#xff0c;而是机器人能力如何在真实世界中逐步可用、可规模化、可自治的演进。一、核心判断&#xff08;一句话&#xff…

作者头像 李华
网站建设 2026/2/9 20:58:13

AnimeGANv2多平台适配:Windows/Linux部署统一镜像

AnimeGANv2多平台适配&#xff1a;Windows/Linux部署统一镜像 1. 技术背景与项目定位 随着AI生成技术的快速发展&#xff0c;风格迁移&#xff08;Style Transfer&#xff09;在图像处理领域展现出强大的应用潜力。其中&#xff0c;将真实照片转换为二次元动漫风格的需求日益…

作者头像 李华
网站建设 2026/2/15 9:14:11

5分钟玩转AI艺术:用「AI印象派工坊」一键生成4种艺术风格

5分钟玩转AI艺术&#xff1a;用「AI印象派工坊」一键生成4种艺术风格 关键词&#xff1a;AI艺术、OpenCV、非真实感渲染、图像风格迁移、WebUI 摘要&#xff1a;在AI技术不断渗透创意领域的今天&#xff0c;如何以极简方式实现高质量的艺术化图像生成&#xff1f;本文介绍一款基…

作者头像 李华
网站建设 2026/2/10 17:19:51

【数据库】【Mysql】MySQL 索引优化深度解析:从原理到实战

MySQL 索引优化深度解析&#xff1a;从原理到实战 在 MySQL 性能优化体系中&#xff0c;索引是提升查询效率的核心武器。本文将深入剖析五大关键技术&#xff1a;复合索引最左前缀原则、覆盖索引、索引下推&#xff08;ICP&#xff09;、MRR&#xff08;Multi-Range Read&#…

作者头像 李华