C++ 多线程同步之条件变量(condition_variable)实战

C++ 多线程同步之条件变量(condition_variable)实战

C++ 多线程同步之条件变量(condition_variable)实战

在这里插入图片描述

💡 学习目标:掌握 C++ 标准库中条件变量的使用方法,理解条件变量与互斥锁的协同工作机制,能够解决多线程间的等待-通知问题。
💡 学习重点std::condition_variable 的核心接口、wait()notify_one()/notify_all() 的配合使用、生产者-消费者模型的实现。

49.1 条件变量的引入场景

在多线程编程中,我们经常会遇到线程需要等待某个条件满足后再执行的场景。
比如生产者线程生产数据后,消费者线程才能消费;队列不为空时,消费者才能从中取数据。
如果仅用互斥锁实现,消费者线程只能不断轮询检查条件,这会造成 CPU 资源的浪费。

⚠️ 注意事项:单纯的轮询会导致 CPU 空转,降低程序运行效率,条件变量就是为解决这类问题而生的。

举个简单的轮询反例,消费者不断检查队列是否有数据:

#include<iostream>#include<thread>#include<mutex>#include<queue>usingnamespace std; queue<int> data_queue; mutex mtx;// 生产者voidproducer(){for(int i =1; i <=5;++i){ lock_guard<mutex>lock(mtx); data_queue.push(i); cout <<"生产者生产数据:"<< i << endl;}}// 消费者(轮询方式)voidconsumer(){while(true){ lock_guard<mutex>lock(mtx);if(!data_queue.empty()){int data = data_queue.front(); data_queue.pop(); cout <<"消费者消费数据:"<< data << endl;if(data ==5)break;}// 没有数据时,依然会不断循环检查,浪费CPU}}intmain(){ thread t_producer(producer); thread t_consumer(consumer); t_producer.join(); t_consumer.join();return0;}

运行该程序,消费者线程在队列空的时候会一直循环检查,造成不必要的 CPU 开销。

49.2 C++ 标准库中的条件变量

C++11 标准库在 <condition_variable> 头文件中提供了 std::condition_variable 类,它需要与 std::mutex 配合使用,实现线程间的高效等待与通知。

49.2.1 std::condition_variable 的核心接口

  1. wait(unique_lock& lck)
    • 调用该函数的线程会释放持有的互斥锁,并进入阻塞状态。
    • 直到被其他线程的 notify_one()notify_all() 唤醒。
    • 唤醒后,线程会重新获取互斥锁,然后继续执行。
  2. wait(unique_lock& lck, Predicate pred)
    • 带条件的等待,只有当 pred 条件为 false 时才会阻塞。
    • 被唤醒后会先检查条件,条件满足才会继续执行,否则再次阻塞。
    • 该重载可以避免虚假唤醒问题。
  3. notify_one()
    • 唤醒一个正在等待该条件变量的线程。
    • 如果有多个线程等待,随机唤醒其中一个。
  4. notify_all()
    • 唤醒所有正在等待该条件变量的线程。

49.2.2 搭配 std::unique_lock 的原因

std::condition_variablewait() 函数要求传入 std::unique_lock,而不是 std::lock_guard
这是因为 wait() 过程中需要临时释放锁,而 std::unique_lock 支持手动解锁和加锁,std::lock_guard 仅支持构造加锁、析构解锁,无法满足需求。

核心结论:条件变量必须与 std::unique_lock 配合使用,才能实现等待时释放锁、唤醒后重新加锁的逻辑。

49.3 条件变量实战:解决等待-通知问题

我们使用 std::condition_variable 改造 49.1 节的轮询反例,实现高效的生产者-消费者模型:

#include<iostream>#include<thread>#include<mutex>#include<queue>#include<condition_variable>usingnamespace std; queue<int> data_queue; mutex mtx; condition_variable cv;bool is_produced =false;// 生产完成标志// 生产者voidproducer(){for(int i =1; i <=5;++i){ lock_guard<mutex>lock(mtx); data_queue.push(i); cout <<"生产者生产数据:"<< i << endl;} is_produced =true; cv.notify_all();// 生产完成,唤醒所有等待的消费者}// 消费者(条件变量方式)voidconsumer(){ unique_lock<mutex>lock(mtx);// 等待条件:队列不为空 或 生产已完成 cv.wait(lock,[](){return!data_queue.empty()|| is_produced;});while(!data_queue.empty()){int data = data_queue.front(); data_queue.pop(); cout <<"消费者消费数据:"<< data << endl;}}intmain(){ thread t_producer(producer); thread t_consumer(consumer); t_producer.join(); t_consumer.join();return0;}

运行该程序,消费者线程在没有数据时会进入等待状态,不会浪费 CPU 资源。
生产者生产完成后唤醒消费者,消费者再进行数据消费。

49.3.1 解决虚假唤醒问题

虚假唤醒指的是线程在没有被 notify_one()/notify_all() 唤醒的情况下,也可能从 wait() 中返回。
为了避免这种情况,我们必须使用带条件的 wait() 重载版本,通过判断条件是否满足来决定是否继续执行。

例如,在消费者线程中,我们用 cv.wait(lock, [](){ return !data_queue.empty() || is_produced; }) 替代无参的 wait(),确保只有在队列有数据或生产完成时,线程才会被唤醒并继续执行。

49.4 实战案例:多生产者-多消费者模型

我们实现一个支持多个生产者和多个消费者的模型,使用条件变量保证线程间的同步协作:

#include<iostream>#include<thread>#include<mutex>#include<queue>#include<condition_variable>#include<vector>usingnamespace std;constint MAX_QUEUE_SIZE =5;// 队列最大容量 queue<int> data_queue; mutex mtx; condition_variable cv_producer;// 生产者条件变量 condition_variable cv_consumer;// 消费者条件变量bool stop_flag =false;// 停止标志// 生产者函数voidproducer_func(int id){for(int i =1; i <=3;++i){ unique_lock<mutex>lock(mtx);// 等待队列有空位 cv_producer.wait(lock,[](){return data_queue.size()< MAX_QUEUE_SIZE || stop_flag;});if(stop_flag)break;int data = id *10+ i; data_queue.push(data); cout <<"生产者"<< id <<"生产数据:"<< data <<",队列大小:"<< data_queue.size()<< endl; cv_consumer.notify_one();// 唤醒一个消费者}}// 消费者函数voidconsumer_func(int id){while(true){ unique_lock<mutex>lock(mtx);// 等待队列有数据 cv_consumer.wait(lock,[](){return!data_queue.empty()|| stop_flag;});if(stop_flag && data_queue.empty())break;int data = data_queue.front(); data_queue.pop(); cout <<"消费者"<< id <<"消费数据:"<< data <<",队列大小:"<< data_queue.size()<< endl; cv_producer.notify_one();// 唤醒一个生产者}}intmain(){// 创建 2 个生产者线程和 3 个消费者线程 vector<thread> producers; vector<thread> consumers;for(int i =1; i <=2;++i){ producers.emplace_back(producer_func, i);}for(int i =1; i <=3;++i){ consumers.emplace_back(consumer_func, i);}// 等待所有生产者完成for(auto& t : producers){ t.join();}// 设置停止标志,唤醒所有消费者 stop_flag =true; cv_consumer.notify_all();// 等待所有消费者完成for(auto& t : consumers){ t.join();} cout <<"所有生产和消费任务完成"<< endl;return0;}

运行效果

  1. 生产者线程会在队列满时等待,队列有空位时继续生产。
  2. 消费者线程会在队列空时等待,队列有数据时继续消费。
  3. 生产完成后设置停止标志,唤醒所有消费者线程并退出,避免线程阻塞。

49.5 条件变量与互斥锁的协同要点

  1. 条件变量必须搭配互斥锁使用wait() 函数需要先获取互斥锁,才能保证条件判断的线程安全。
  2. 优先使用带条件的 wait():可以有效避免虚假唤醒,确保线程在正确的条件下被唤醒。
  3. notify_one()notify_all() 的选择
    • 当只需要唤醒一个等待线程时,使用 notify_one(),效率更高。
    • 当需要唤醒所有等待线程时,使用 notify_all(),比如生产完成后通知所有消费者。

49.6 本章小结

  1. 条件变量用于解决多线程间的等待-通知问题,避免了轮询造成的 CPU 资源浪费。
  2. std::condition_variable 必须与 std::unique_lock 配合使用,核心接口是 wait()notify_one()notify_all()
  3. 带条件的 wait() 重载版本可以解决虚假唤醒问题,是实际开发中的首选。
  4. 生产者-消费者模型是条件变量的典型应用场景,通过合理设计条件可以实现高效的线程协作。

Read more

Leaflet赋能:WebGIS视角下的省域区县天气可视化实战攻略

Leaflet赋能:WebGIS视角下的省域区县天气可视化实战攻略

目录 前言 一、空间数据基础 1、省域空间检索 2、区县天气信息检索 二、天气数据简介 1、省域天气数据获取 2、区县名称不一致 三、SpringBoot后台实现 1、Java后台天气数据查询 2、控制层实现 四、WebGIS前端实现 1、气温颜色及图例初始化 2、气温数据展示实现 五、成果展示 1、湖南省天气展示 2、西藏自治区天气展示 六、总结 前言         在当今数字化时代,地理信息系统(GIS)技术与Web技术的深度融合,为地理信息的可视化展示带来了前所未有的机遇。WebGIS作为一种基于网络的地理信息系统,能够将地理空间数据以直观、便捷的方式呈现给用户,极大地拓展了地理信息的应用范围和价值。而天气数据作为与人们生活息息相关的重要地理信息之一,其可视化展示对于气象预报、灾害预警、交通规划、农业生产等诸多领域都有着极为重要的意义。本文将从WebGIS的视角出发,

By Ne0inhk
cann-recipes-train 仓库深度解读:昇腾平台下 DeepSeek-R1 与 Qwen2.5 强化学习训练优化实践

cann-recipes-train 仓库深度解读:昇腾平台下 DeepSeek-R1 与 Qwen2.5 强化学习训练优化实践

cann-recipes-train 仓库深度解读:昇腾平台下 DeepSeek-R1 与 Qwen2.5 强化学习训练优化实践 前言 自 DeepSeek-R1 发布以来,大模型的强化学习(RL)训练掀起了新一轮的技术热潮。各大厂商与开源社区纷纷投入实践,持续探索更高效的 RL 训练体系。本文将基于 cann-recipes-train 仓库,解读两个实践样例:DeepSeek-R1 的 RL 训练优化实践样例、基于 verl 框架的 Qwen2.5 强化学习实践样例 cann-recipes-train 仓库全景解析:昇腾训练优化的"实战底座" 大模型训练拼效率的阶段,CANN 直接帮我们搞定了底层异构硬件适配、资源调度这些麻烦事,不用再从零研究 GPU 和 NPU 怎么协同,现有模型代码也不用大改就能对接,训

By Ne0inhk
前端引入的JS加载失败页面功能无法使用?JS加载失败的终极解决方案

前端引入的JS加载失败页面功能无法使用?JS加载失败的终极解决方案

🌷 古之立大事者,不惟有超世之才,亦必有坚忍不拔之志 🎐 个人CSND主页——Micro麦可乐的博客 🐥《Docker实操教程》专栏以最新的Centos版本为基础进行Docker实操教程,入门到实战 🌺《RabbitMQ》专栏19年编写主要介绍使用JAVA开发RabbitMQ的系列教程,从基础知识到项目实战 🌸《设计模式》专栏以实际的生活场景为案例进行讲解,让大家对设计模式有一个更清晰的理解 🌛《开源项目》本专栏主要介绍目前热门的开源项目,带大家快速了解并轻松上手使用 🍎 《前端技术》专栏以实战为主介绍日常开发中前端应用的一些功能以及技巧,均附有完整的代码示例 ✨《开发技巧》本专栏包含了各种系统的设计原理以及注意事项,并分享一些日常开发的功能小技巧 💕《Jenkins实战》专栏主要介绍Jenkins+Docker的实战教程,让你快速掌握项目CI/CD,是2024年最新的实战教程 🌞《Spring Boot》专栏主要介绍我们日常工作项目中经常应用到的功能以及技巧,代码样例完整 👍《Spring Security》专栏中我们将逐步深入Spring Security的各个

By Ne0inhk
【测试理论与实践】(十)Web 项目自动化测试实战:从 0 到 1 搭建博客系统 UI 自动化框架

【测试理论与实践】(十)Web 项目自动化测试实战:从 0 到 1 搭建博客系统 UI 自动化框架

目录 前言 一、项目背景与测试规划:先明确 "测什么" 和 "怎么测" 1.1 项目介绍 1.2 测试目标 1.3 测试范围与用例设计 编辑 二、环境搭建:3 步搞定自动化测试前置准备 2.1 安装核心依赖包 2.2 浏览器配置 2.3 项目目录结构设计 三、核心模块开发:封装公共工具,提高代码复用性 3.1 驱动管理与截图工具封装(common/Utils.py) 3.2 代码说明与优化点 四、测试用例开发:

By Ne0inhk