当前位置: 首页 > news >正文

C++条件变量(一):从轮询到唤醒 —— 条件变量的设计动机与基础用法

文章目录

    • 0.引言
    • 1.核心组件与基本 API
    • 2.生产者-消费者示例
    • 3.为什么 wait必须与互斥锁配合使用?
    • 4.notify_one 与 notify_all 的区别
    • 5.谓词版本的 wait 为什么更安全?
    • 6. 小结

0.引言

在多线程编程程序中,线程之间经常需要协同工作。常见的一种场景是:一个线程需要等待某个条件满足,再继续执行。例如:

  • 消费者线程等待队列非空,然后取出数据;

  • 工作线程等待某个标志位被设置,然后开始处理任务。

如果直接用最简单的轮询(busy waiting)来实现:

// 消费者线程while(queue.empty()){// 什么都不做,继续循环}// 退出循环后,队列非空,取出数据

这种写法的问题很明显:CPU 会一直空转,浪费资源,如果系统负载高,这种空转可能导致其他线程得不到执行机会。

条件变量(std::condition_variable)正是为了解决这个问题而生的。它允许线程在条件不满足时休眠,将 CPU 让给其他线程,直到条件满足时被唤醒。这是一种高效的线程同步机制。

1.核心组件与基本 API

C++ 标准库提供了两个条件变量类:

  • std::condition_variable:只与 std::mutex 配合使用。

  • std::condition_variable_any:可与任何满足互斥体概念的对象配合,但开销更大。

在绝大多数情况下,我们使用 std::condition_variable 即可。

主要成员函数:

2.生产者-消费者示例

我们来实现一个最简单的生产者-消费者模型:

  • 生产者线程向队列中放入数据,并通知消费者。

  • 消费者线程等待队列非空,然后取出数据。

#include<iostream>#include<thread>#include<mutex>#include<condition_variable>#include<queue>#include<chrono>std::queue<int>g_queue;// 共享数据队列std::mutex g_mutex;// 保护队列的互斥锁std::condition_variable g_cv;// 条件变量voidproducer(){for(inti=0;i<10;++i){{std::lock_guard<std::mutex>lock(g_mutex);g_queue.push(i);std::cout<<"Produced: "<<i<<std::endl;}// 离开作用域,自动解锁g_cv.notify_one();// 通知一个消费者线程std::this_thread::sleep_for(std::chrono::milliseconds(100));}}voidconsumer(){while(true){std::unique_lock<std::mutex>lock(g_mutex);// 等待,直到队列非空(条件满足)g_cv.wait(lock,[]{return!g_queue.empty();});// 条件满足,取出数据intvalue=g_queue.front();g_queue.pop();std::cout<<"Consumed: "<<value<<std::endl;lock.unlock();// 可选,提前解锁// 模拟消费耗时std::this_thread::sleep_for(std::chrono::milliseconds(150));}}intmain(){std::threadt1(producer);std::threadt2(consumer);t1.join();// 为了让消费者看到结束,此处简单等待(实际项目可使用哨兵值)std::this_thread::sleep_for(std::chrono::seconds(1));// 注意:这里没有优雅退出,只是示例,实际应该设置停止标志return0;}

关键点解释:

  • 生产者使用 std::lock_guard 来锁定 g_mutex,在修改队列后立即释放锁,然后调用 notify_one()。这样唤醒时,消费者线程可以立刻获取锁。

  • 消费者使用 std::unique_lock(而不是 lock_guard),因为 wait 需要在线程阻塞期间解锁,唤醒后再重新锁定,unique_lock 支持这种操作。

  • wait 的谓词版本 g_cv.wait(lock, []{ return !g_queue.empty(); }) 等价于:

while(!g_queue.empty()){g_cv.wait(lock);}

它会在条件不满足时持续等待,即使被虚假唤醒(spurious wakeup)也会重新检查条件。

3.为什么 wait必须与互斥锁配合使用?

这是一个常见的问题:为什么不能单独使用条件变量?为什么 wait 必须接受一个已经锁定的 unique_lock?

考虑一个没有锁的伪代码实现:

// 错误示例:没有使用互斥锁if(queue.empty()){cv.wait();// 假设有这样的 API}

假设线程 A 执行到 if (queue.empty()) 判断为真,正准备进入 wait,但此时操作系统切换到了生产者线程 B。B 修改了队列(使其非空),并调用了 notify_one()。由于 A 还没有进入 wait,这个通知就丢失了。接着 A 才进入 wait,它将永远等下去,因为没有人再通知它了。

这就是经典的 “丢失唤醒” 问题。解决办法是:将条件检查和进入等待这两个步骤原子化。互斥锁实现了这一点:wait 内部会原子地完成“解锁 + 阻塞”两个动作,同时保证条件检查在锁的保护下完成。具体过程如下:

1)线程在调用 wait 之前已经持有锁。

2)wait 内部首先检查条件(谓词版本会先调用谓词,如果不满足,则执行下面的步骤)。

3)原子地:释放锁 + 阻塞线程。

4)当被唤醒后,wait 重新获取锁,然后返回(或再次检查谓词)。

这样,从条件检查到进入休眠之间没有空隙,通知不会丢失。

因此,wait 必须与互斥锁配合,并且锁必须由 std::unique_lock 持有,因为 lock_guard 不支持中途释放锁。

4.notify_one 与 notify_all 的区别

notify_one():唤醒一个等待的线程(如果有多个,由调度器决定唤醒哪一个)。适用于只需一个线程处理新任务的场景,可以减少“惊群效应”。

notify_all():唤醒所有等待的线程。适用于所有线程都需要响应某个状态变化(比如程序结束标志)。

在生产者-消费者模型中,如果只有一个消费者,notify_one 就足够了。如果有多个消费者,且每次生产一个数据,通常也使用 notify_one,因为只有一个消费者能获得数据,其他被唤醒的线程会再次进入等待,造成不必要的上下文切换。

5.谓词版本的 wait 为什么更安全?

上面的示例中,我们使用了 g_cv.wait(lock, []{ return !g_queue.empty(); }); 而不是直接调用 wait(lock) 再自己检查。

理由有两个:

1)自动处理虚假唤醒:即使线程被虚假唤醒,谓词会被重新检查,如果不满足,会继续等待,避免了错误执行。

2)代码更简洁:将条件检查与等待逻辑封装在一起,减少了出错可能。

在 C++ 标准中,wait(lock, pred) 等价于:

while(!pred()){wait(lock);}

因此,它已经包含了必要的循环。

6. 小结

  • 条件变量解决了线程间高效等待的问题,避免了 CPU 空转。

  • 基本用法:互斥锁 + 条件变量,等待条件满足。

  • wait 必须与 std::unique_lock 配合,用于原子地释放锁并阻塞。

  • 使用 notify_one 或 notify_all 唤醒线程。

  • 始终使用谓词版本的 wait,以正确处理虚假唤醒。

下一篇我们将深入探讨条件变量的超时机制。
更多深入内容欢迎了解:C++/Linux/ 数据库内核 | 底层开发 + AI 实战圈——12 个月系统落地,从原理到工业级实战,搭建你的核心技术壁垒

http://www.jsqmd.com/news/623771/

相关文章:

  • 用STM32F4的HAL库搞定电机测速:从编码器接线到RPS计算,一篇就够了
  • 谷歌开源大模型 Gemma 4​ 与智能体框架 OpenClaw​ 结合使用
  • 聊聊2026年口碑好的SPC门来图定制公司,哪家性价比高 - 工业推荐榜
  • 人工智能音乐创作平台版权授权纷争背后的监管隐忧
  • 2026年 AI Agent 深度解析:从 ReAct 范式到 Multi-Agent 协作的工程化落地
  • 新手避坑指南:用Carsim 2020和Matlab 2021b复现ABS联合仿真(从模型导入到动画对比)
  • 3步掌握ChanlunX:让缠论技术分析从复杂到简单的可视化利器
  • 收藏!小白程序员快速入门大模型:23个核心概念轻松掌握
  • Git-RSCLIP遥感图像分类:5分钟零代码上手,卫星图识别不求人
  • 2026年板栗公司推荐及选购参考 - 品牌策略师
  • 在超大数据集下 DuckDB 与 MySQL 查询速度对比绿
  • 3个核心技术深度破解Cursor免费限制:AI代码编辑器的无限使用方案
  • 如何在Windows电脑上快速安装APK文件:告别模拟器的终极指南
  • ARM平台下libcrypto.so.1.0.0的交叉编译避坑指南
  • 3分钟从文档到专业演示文稿:PPTAgent让你的PPT制作效率提升300倍
  • League-Toolkit终极指南:英雄联盟玩家的智能游戏助手解决方案
  • 用普通摄像头实现心率监测?手把手教你搭建RPPG实验环境(Python+OpenCV实战)
  • Roboto字体架构深度解析:现代无衬线字体的工程实现
  • 安徽诚鑫物资回收有限公司:合肥蜀山区专业承接电缆 有色金属回收电话 - LYL仔仔
  • Python气象绘图库Meteva避坑指南:从站点插值到地图叠加的13个实战问题修复
  • 3分钟掌握Vue大屏自适应:终极解决方案让复杂布局轻松适配
  • springboot 微信小程序的红色导览之烈士陵园烈士纪念app
  • Windsurf的Write和Chat模式怎么选?一篇讲清点数消耗、模型降级和你的真实开发场景
  • VR-Reversal:免费实现3D视频转2D播放的终极解决方案
  • WorkshopDL:打破平台壁垒的终极Steam创意工坊下载器,三步免费获取任何游戏模组
  • PaddleOCR文本矫正模块深度体验:从UVDoc模型推理到高性能模式(HPI)配置全解析
  • 再次革新 .NET 的构建和发布方式(三)卦
  • DsHidMini技术实战指南:Windows系统下的PS3手柄驱动深度配置
  • FOGProject:企业级设备克隆与管理的开源解决方案
  • 3分钟快速诊断网络NAT类型:NatTypeTester完整指南