C++11线程库 (六) 条件变量 Condition variables

2023-10-27

一、什么是条件变量?

条件变量类(condition_variable)是一个同步原语,它可以在同一时间阻塞一个线程或者多个线程,直到其他线程改变了共享变量(条件)并通知。

primitive 原语,表达的是基础、基本的,是其他复杂应用的构建基础。

二、为什么需要条件变量?

  • 减少轮询从而提高效率。没有条件变量,CPU会浪费时间反复轮询某一个条件。条件变量出现使得线程可以在不满足条件进行休眠,将资源让给有需要的其他线程;
  • 线程之间的协调。两个线程之间执行可以变得有序,也就是一个线程执行完成后,另外一个线程才会执行;
  • 复杂同步条件。有时候一个线程不仅需要知道资源可达,还需要知道一些额外信息,比如说生产消费者中,缓冲队列是否满之类的操作;

线程进入阻塞可以是sleep,也可以是wait。sleep期间所持有的锁不会释放,到时见会自动唤醒,而wait期间将会释放锁,唤醒还需要额外的条件。前者更多的用在模拟操作延迟,后者用于多线程协作场景。

试想一个场景,两个线程th1和th2,他们分别执行这两个不同的变量,th1的继续执行必须等到th2完成其计算过程才可以继续执行,那么将会有两种策略:

  • 第一种,轮询th2是否执行标志位flag,th1种不断轮询标志位是否为true;
  • 第二种,th1和th2通过条件变量进行联系,th1根据th2是否通知决定其是等待还是继续执行;

第一种方法,最简单的实现方法就是:

void th1_fun()
{
	while(!flag)
	{
		std::this_thread::sleep_for(10ms);
	}
	//continue execute th1
}

检测到flag为false,线程进入休眠;检测到flag为true,退出while阻塞,继续执行,这个线程休眠是为了让其他线程有机会执行,也就是有机会让flag变成true,休眠时间如果过长,会导致响应延迟,如果休眠时间过短,cpu时间将会大部分浪费在判断flag是否改变。

第二种方法就是这里提到的条件变量,条件变量和轮询不一样的地方在于,他是用等待替代休眠的,这也就是说cpu不需要重复判断条件变量是否成立,而是通过另一个线程主动通知的方式告知无需继续等待。

三、std::condition_variable

头文件#include <condition_variable>

3.1 条件变量是如何完成同步的?

  • 一个线程完成对数据的修改,另一个线程进行等待
  • 数据修改完成,通知另一个线程已经完成修改
  • 等待的线程收到通知,继续进行线程的执行

我们把对数据进行修改的线程称为通知线程,等待通知的线程称为等待线程。

3.2 通知端的具体工作

  • 获取一个std::mutex防止变量写冲突(通常是std::lock_guard);
  • 对变量值进行修改;
  • 对条件变量调用notify_one或者notify_all通知(通知的时候不一定要处于锁定);

3.3 等待端的具体工作

  • 获得一个std::unique_lock<std::mutex>,这个互斥锁与通知端的相同;
  • 条件变量执行wait wait_forwait_until,等待操作将原子地完成释放互斥锁挂起线程操作;
  • 当条件变量被通知时,超时或者虚假唤醒,线程将会被唤醒,互斥锁再次被获取,线程需要检查条件和假如时虚假唤醒则继续等待。

!!std::condition_variable 只可与 std::unique_lock<std::mutex> 一同使用;此限制能让这种机制在一些平台取得最高的效率。当然如果你想使用与锁类型无关的条件变量,可以试试他的兄弟::std::condition_variable_any。注意等待端释放和挂起是原子操作,具体原因点这里

3.4 成员函数

构造函数只有无参默认构造函数,无拷贝、无赋值。剩下的就是两类成员函数,一是通知方法,二是等待方法。

  • 通知方法:要么通知所有人notify_one,要么通知一个人notify_all

  • 等待方法:等待wait、等待一段时间wait_for和等待某个时刻wait_until。使用的方式就是“条件变量调用某种方法等待unique_lcok”

cond.wait(ulo);

调用wait方法导致当前线程阻塞直至条件变量被通知,或虚假唤醒发生,可选地循环直至满足某谓词。为了避免虚假唤醒,你可以使用谓词

template< class Predicate >
void wait( std::unique_lock<std::mutex>& lock, Predicate pred );

等价于以下语句:

while (!pred()) {//不满足继续等待
    wait(lock);
}

这个谓词应该理解为解除阻塞条件。

四、条件变量实例

这个例子实现了按序打印并发线程:

class ConditionVarTest
{
    public:
    void printOne()
    {
        
        for(int i=0;i<10;i++)
        {   
            std::this_thread::sleep_for(1ms); //必要,因为可能消费者没有处理完数据就已经生产了下一个数据,并下发了另一个通知。
            m_i=i;
            dataReady=true;
            cond.notify_all();
        }
    }
    void printTwo()
    {
        for(int i=0;i<10;i++)
        {
            std::unique_lock<std::mutex> ulo(lo);
            cond.wait(ulo,[this]{return dataReady==true;});
            
            std::cout<<"Get data value = "<<m_i<<std::endl;
            dataReady=false;
        }

        
    }

    std::mutex lo;
    std::condition_variable cond;

    private:
    bool dataReady=false;
    int m_i=0;

};


int main()
{
    ConditionVarTest v;
    std::thread th1(&ConditionVarTest::printOne,&v);
    std::thread th2(&ConditionVarTest::printTwo,&v);
    th1.join();
    th2.join();

}

五、虚假唤醒

条件变量还应该注意的地方在于虚假唤醒,也就是生产者发了通知,但是没有产品。

这常常出现在生产-消费设计模式中,假设消费者处理速度足够快,一个生产者一次只生产一个商品,有以下结论:

  • 一个生产者,一个消费者,notify_one notify_all都不会出现虚假唤醒。此时notify_one等于notify_all,只通知一个自然不会出现虚假唤醒;
  • 一个生产者,多个消费者,notify_one不会出现虚假唤醒,notify_all会出现。消费者可能同时收到商品到货通知,但是货物只可能有一个,其他消费者被唤醒,但是没有商品可供消费;
  • 多个生产者,一个消费者,notify_one notify_all都不会出现虚假唤醒。此时notify_one等于notify_all,你多次通知给同一个消费者,消费者中总是有商品可供消费;
  • 多个生产者,多个消费者,notify_one notify_all都会出现虚假唤醒。多个生产者发出notify_one,如果一个消费者处理完所有货物(黄牛),其他消费者将无货可用;多个生产者发出notify_all更加是如此。

小结:如果消费者只有一个,那么无论如何都不可能出现虚假唤醒;多生产者,多消费者,无论如何都会出现虚假唤醒;生产者只有一个,消费者多个,notify_one不会,而notify_all会出现。

如何处理虚假唤醒?在消费者唤醒之后,利用while反复对产品可用性进行检查,如果不可用继续进行wait进入等待唤醒状态。当然,C++11已经帮你处理好这种情况了,也就是wait的第二个参数。


[1] https://www.jianshu.com/p/01ad36b91d39
[2] https://blog.csdn.net/shizheng163/article/details/83661861
[3] https://www.zhihu.com/question/42962803/answer/120217624
[4] https://en.cppreference.com/w/cpp/thread/condition_variable
[5] https://zhuanlan.zhihu.com/p/422670024

20211021 修改了例子,和虚假唤醒部分描述。
20211029 线程阻塞有两种情况,一个是休眠,另一个是挂起。休眠是在特时刻恢复运行,挂起是等待某个条件后恢复运行。休眠不知道继续执行的具体时间,挂起则切确知道回复运行的时刻。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

C++11线程库 (六) 条件变量 Condition variables 的相关文章

  • 将复选框添加到 UniformGrid

    我正在尝试将复选框动态添加到 wpf 中的统一网格中 但看起来网格没有为它们分配足够的空间 所以它们都有点互相重叠 这就是我将它们添加到后面的代码中的方法 foreach string folder in subfolders PathCh
  • 无法使用已与其底层 RCW 分离的 COM 对象。在 oledb 中

    我收到此错误 但我不知道我做错了什么 下面的代码在backrgroundworker中 将异常详细信息复制到剪贴板 System Runtime InteropServices InvalidComObjectException 未处理 通
  • 访问私人成员[关闭]

    Closed 这个问题是基于意见的 help closed questions 目前不接受答案 通过将类的私有成员转换为 void 指针 然后转换为结构来访问类的私有成员是否合适 我认为我无权修改包含我需要访问的数据成员的类 如果不道德 我
  • 是否可以强制 XMLWriter 将元素写入单引号中?

    这是我的代码 var ptFirstName tboxFirstName Text writer WriteAttributeString first ptFirstName 请注意 即使我使用 ptFirstName 也会以双引号结束 p
  • ASP.NET Core Serilog 未将属性推送到其自定义列

    我有这个设置appsettings json对于我的 Serilog 安装 Serilog MinimumLevel Information Enrich LogUserName Override Microsoft Critical Wr
  • 获取按下的按钮的返回值

    我有一个在特定事件中弹出的表单 它从数组中提取按钮并将标签值设置为特定值 因此 如果您要按下或单击此按钮 该函数应返回标签值 我怎样才能做到这一点 我如何知道点击了哪个按钮 此时代码返回 DialogResult 但我想从函数返回 Tag
  • pthread_cond_timedwait() 和 pthread_cond_broadcast() 解释

    因此 我在堆栈溢出和其他资源上进行了大量搜索 但我无法理解有关上述函数的一些内容 具体来说 1 当pthread cond timedwait 因为定时器值用完而返回时 它如何自动重新获取互斥锁 互斥锁可能被锁定在其他地方 例如 在生产者
  • WPF 中的调度程序和异步等待

    我正在尝试学习 WPF C 中的异步编程 但我陷入了异步编程和使用调度程序的困境 它们是不同的还是在相同的场景中使用 我愿意简短地回答这个问题 以免含糊不清 因为我知道我混淆了 WPF 中的概念和函数 但还不足以在功能上正确使用它 我在这里
  • C#:如何防止主窗体过早显示

    在我的 main 方法中 我像往常一样启动主窗体 Application EnableVisualStyles Application SetCompatibleTextRenderingDefault false Application
  • 如何返回 json 结果并将 unicode 字符转义为 \u1234

    我正在实现一个返回 json 结果的方法 例如 public JsonResult MethodName Guid key var result ApiHelper GetData key Data is stored in db as v
  • C# 中的递归自定义配置

    我正在尝试创建一个遵循以下递归结构的自定义配置部分
  • 从路径中获取文件夹名称

    我有一些路c server folderName1 another name something another folder 我如何从那里提取最后一个文件夹名称 我尝试了几件事 但没有成功 我只是不想寻找最后的 然后就去休息了 Thank
  • 当操作繁忙时,表单不执行任何操作(冻结)

    我有一个使用 C 的 WinForms 应用程序 我尝试从文件中读取一些数据并将其插入数据表中 当此操作很忙时 我的表单冻结并且无法移动它 有谁知道我该如何解决这个问题 这可能是因为您在 UI 线程上执行了操作 将文件和数据库操作移至另一个
  • Discord.net 无法在 Linux 上运行

    我正在尝试让在 Linux VPS 上运行的 Discord net 中编码的不和谐机器人 我通过单声道运行 但我不断收到此错误 Unhandled Exception System Exception Connection lost at
  • 实体框架 4 DB 优先依赖注入?

    我更喜欢创建自己的数据库 设置索引 唯一约束等 使用 edmx 实体框架设计器 从数据库生成域模型是轻而易举的事 现在我有兴趣使用依赖注入来设置一些存储库 我查看了 StackOverflow 上的一些文章和帖子 似乎重点关注代码优先方法
  • 如何使我的表单标题栏遵循 Windows 深色主题?

    我已经下载了Windows 10更新包括黑暗主题 文件资源管理器等都是深色主题 但是当我创建自己的 C 表单应用程序时 标题栏是亮白色的 如何使我自己的桌面应用程序遵循我在 Windows 中设置的深色主题 你需要调用DwmSetWindo
  • C - 直接从键盘缓冲区读取

    这是C语言中的一个问题 如何直接读取键盘缓冲区中的数据 我想直接访问数据并将其存储在变量中 变量应该是什么数据类型 我需要它用于我们研究所目前正在开发的操作系统 它被称为 ICS OS 我不太清楚具体细节 它在 x86 32 位机器上运行
  • 如何使用 std::string 将所有出现的一个字符替换为两个字符?

    有没有一种简单的方法来替换所有出现的 in a std string with 转义 a 中的所有斜杠std string 完成此操作的最简单方法可能是boost字符串算法库 http www boost org doc libs 1 46
  • 如何在 C++ BOOST 中像图形一样加载 TIFF 图像

    我想要加载一个 tiff 图像 带有带有浮点值的像素的 GEOTIFF 例如 boost C 中的图形 我是 C 的新手 我的目标是使用从源 A 到目标 B 的双向 Dijkstra 来获得更高的性能 Boost GIL load tiif
  • 使用按位运算符相乘

    我想知道如何使用按位运算符将一系列二进制位相乘 但是 我有兴趣这样做来查找二进制值的十进制小数值 这是我正在尝试做的一个例子 假设 1010010 我想使用每个单独的位 以便将其计算为 1 2 1 0 2 2 1 2 3 0 2 4 虽然我

随机推荐

  • openwrt安装打印机服务器_原创

    作者 水滴实验室 单位 恒安嘉新 前言 随着物联网时代逐步的发展 设备之间的联系更为紧密 每个节点都无法独立存在 而与我们每个人都 息息相关的一些设备 路由器 摄像头 打印机等越来越多的影响着我们的生活各个方面 小到个人隐 私 大到敌对势力
  • 如何在Linux下配置网络访问外网

    Linux下的网络配置 1 查看自己的网卡编号 2 设置使用的网卡 IP地址 网关等参数 3 设置DNS服务器 4 重启网络服务 5 测试是否能ping通外网 6 无法Ping通的异常处理 7 Ping 外网的时候提示Temporary f
  • 鸿蒙3.0应用开发体验

    鸿蒙os3 0发布以来 华为官方开始主推ets arkui开发模式 逐渐抛弃java 为以后去安卓化做铺垫 但目前在笔者体验来看 仍需要大力完善 还有很长的路要走 什么是ets ts是js的超集 而ets是ts的超集 ets后缀的文件中可以
  • Django 迁移数据、迁移服务

    本文介绍两种常用的 Django 服务迁移数据方法 这两种方法都需要在新的服务器部署好数据库 创建好相应的数据库表和用户以后再进行 1 使用dumpdata命令 针对数据量不是很大的项目 可以使用此方法 操作起来比较简单 1 1 数据导出
  • 简单了解IPv4编址

    目录 一 IPv4地址 二 进制转换 三 有类IPv4 四 无类IPv4 4 1 子网掩码 4 2 地址规划 4 3 VLSM可变长子网掩码 五 私有IPv4地址 六 IPv4报文格式 七 IP地址解析 一 IPv4地址 IPv4地址由 网
  • Vue2项目中使用高德地图自定义(Marker)标记点和创建(MassMarks)海量标记点

    前言 本篇文章就是单独分享一下在Vue2项目中如何自定义创建marker标记点和针对要创建庞大数量标记点时所采用的API 能够快速创建数量庞大的marker 不至于在浏览器渲染时产生卡顿的现象 需要了解如何在Vue2项目中引入高德地图请参照
  • [c++/java]递归系列

    本系列是根据个人的做题总结出来 或许有不对之处 望给位大佬指出 同时这个系列也是长期的一个系列 每遇到一个优秀的递归题目 我都会添加上去 基本递归思路 递归之结束条件 个人认为在写递归之前应该首先考虑什么时候递归结束 或者是递归收敛于什么条
  • 有一台电脑怎么挣钱_游戏搬砖就能躺着挣钱?“我就一台电脑,要求不高月入过万就好”...

    随着网络游戏的日益普及 游戏搬砖 已经和代练 陪玩一样 成为很多玩家专职 兼职的工作 对于这样的一份职业 有人不屑 有人眼浅 更多人在看了有搬砖党换一套海景房的新闻 觉得这个职业就是个可以一夜暴富或者躺着挣钱的职业 因而也想自己下海尝试 事
  • 基于stm32作品设计:多功能氛围灯、手机APP无线控制ws2812,MCU无线升级程序

    文章目录 一 作品背景 二 功能设计与实现过程 三 实现基础功能 一 首先是要选材 二 原理图设计 二 第一版本PCB设计 三 焊接PCB板 四 编写单片机程序 五 下载程序验证 四 外壳设计 一 CAD图纸设计 二 磨砂亚克力板 五 重新
  • [Git] 代码管理之 Git(六)Git rebase 压缩提交历史

    我们在工作中 可能会出现这样的情况 一项工作由好几个同事同时完成 然后每个人针对当前的feature都有对应的提交 那么就会造成同一个feature有多次提交的这样的冗余存在 除此之外 如果我们自己针对同一个feature的每天的提交以及一
  • JVM(六)方法调用(补充知识)

    方法调用并不等同于方法中的代码被执行 方法调用阶段唯一的任务就是确定被调用方法的版本 即调用哪一个方法 暂时还未涉及方法内部的具体运行过程 一切方法调用在Class文件里面存储的都只是符号引用 而不是方法在实际运行时内存布局中的入口地址 也
  • 关于H264相关的EBSP,RBSP,SODP的说明

    1 关于H264相关的EBSP RBSP SODP的说明 1 EBSP 扩展字节序列载荷 Encapsulated Byte Sequence Payload 它去掉了00 00 01 00 00 00 01这些起始码 但包含了0x3防止竞
  • 时序约束优先级_静态时序分析圣经翻译计划——附录A:SDC

    本附录将介绍1 7版本的SDC格式 此格式主要用于指定设计的时序约束 它不包含任何特定工具的命令 例如链接 link 和编译 compile 它是一个文本文件 可以手写或由程序创建 并由程序读取 某些SDC命令仅适用于实现 implemen
  • @Around简单使用示例——SpringAOP增强处理

    Around的作用 既可以在目标方法之前织入增强动作 也可以在执行目标方法之后织入增强动作 可以决定目标方法在什么时候执行 如何执行 甚至可以完全阻止目标目标方法的执行 可以改变执行目标方法的参数值 也可以改变执行目标方法之后的返回值 当需
  • 如何搭建Spring开发环境呢?

    转自 如何搭建Spring开发环境呢 下文讲述搭建Spring开发环境的方法分享 如下所示 由于Spring是基于Java代码的一个框架 所以在Spring环境搭建之前 我们需为开发环境安装好 JDK Java开发环境 Eclipse Ja
  • 来自ebay内部的「软件测试」学习资料,覆盖GUI、API自动化、代码级测试及性能测试等,Python等,拿走不谢!...

    在软件测试领域从业蛮久了 常有人会问我 刚入测试一年 很迷茫 觉得没啥好做的 测试在公司真的不受重视 我是不是去转型做开发会更好 资深的测试架构师的发展路径是怎么样的 我平时该怎么学习 我估计不少人有这样的想法 甚至你也会被身边的人所影响
  • React 中constructor 作用

    React 中constructor 作用 react中的constructor大体有两个作用 1 初始化this state 2 纠正方法的this的指向 constructor props super props this state
  • 大数据毕设 python+深度学习+opencv实现植物识别算法系统

    文章目录 0 前言 2 相关技术 2 1 VGG Net模型 2 2 VGG Net在植物识别的优势 1 卷积核 池化核大小固定 2 特征提取更全面 3 网络训练误差收敛速度较快 3 VGG Net的搭建 3 1 Tornado简介 1 优
  • LWIP学习笔记(2)---IP协议实现细节

    IP头 收到的数据首先保存在pbuf结构中 The IPv4 header struct ip hdr version header length PACK STRUCT FLD 8 u8 t v hl type of service PA
  • C++11线程库 (六) 条件变量 Condition variables

    一 什么是条件变量 条件变量类 condition variable 是一个同步原语 它可以在同一时间阻塞一个线程或者多个线程 直到其他线程改变了共享变量 条件 并通知 primitive 原语 表达的是基础 基本的 是其他复杂应用的构建基