C++11特性:线程同步之条件变量

条件变量是C++11提供的另外一种用于等待的同步机制,它能阻塞一个或多个线程,直到收到另外一个线程发出的通知或者超时时,才会唤醒当前阻塞的线程。条件变量需要和互斥量配合起来使用,C++11提供了两种条件变量:

1. condition_variable:需要配合std::unique_lock<std::mutex>进行wait操作,也就是阻塞线程的操作。
2. condition_variable_any:可以和任意带有lock()unlock()语义的mutex搭配使用,也就是说有四种:
1. std::mutex:独占的非递归互斥锁。
2. std::timed_mutex:带超时的独占非递归互斥锁。
3. std::recursive_mutex:不带超时功能的递归互斥锁。
4. std::recursive_timed_mutex:带超时的递归互斥锁。

条件变量通常用于生产者和消费者模型,大致使用过程如下:

1. 拥有条件变量的线程获取互斥量。
2. 循环检查某个条件,如果条件不满足阻塞当前线程,否则线程继续向下执行。
        2.1 产品的数量达到上限生产者阻塞,否则生产者一直生产。
        2.2 产品的数量为零消费者阻塞,否则消费者一直消费。
3. 条件满足之后,可以调用notify_one()或者notify_all()唤醒一个或者所有被阻塞的线程。
        3.1 由消费者唤醒被阻塞的生产者,生产者解除阻塞继续生产。
        3.2 由生产者唤醒被阻塞的消费者,消费者解除阻塞继续消费。

 1. condition_variable

1.1 成员函数:

condition_variable的成员函数主要分为两部分:线程等待(阻塞)函数线程通知(唤醒)函数,这些函数被定义于头文件 <condition_variable>

1.1.1 等待函数:

调用wait()函数的线程会被阻塞。函数原型如下:

// ①
void wait (unique_lock<mutex>& lck);
// ②
template <class Predicate>
void wait (unique_lock<mutex>& lck, Predicate pred);

函数①:调用该函数的线程直接被阻塞

函数②:该函数的第二个参数是一个判断条件,是一个返回值为布尔类型函数

        ②.1:该参数可以传递一个有名函数的地址,也可以直接指定一个匿名函数
        ②.2:表达式返回false当前线程被阻塞,表达式返回true当前线程不会被阻塞,继续向下执行。 

注意:
独占的互斥锁对象不能直接传递wait()函数,需要通过模板类unique_lock进行二次处理,通过得到的对象仍然可以对独占的互斥锁对象做如下操作,使用起来更灵活。

1. lock():锁定关联的互斥锁。

2. try_lock():尝试锁定关联的互斥锁,若无法锁定,函数直接返回。

3. try_lock_for():试图锁定关联的可定时锁定互斥锁,若互斥锁在给定时长中仍不能被锁定,函数返回。

 4. try_lock_until():试图锁定关联的可定时锁定互斥锁,若互斥锁在给定的时间点后仍不能被锁定,函数返回。

5. unlock():将互斥锁解锁。

如果线程被该函数阻塞,这个线程会释放占有的互斥锁的所有权,当阻塞解除之后这个线程会重新得到互斥锁的所有权,继续向下执行(这个过程是在函数内部完成的,了解这个过程即可,其目的是为了避免线程的死锁)。

wait_for()函数和wait()的功能是一样的,只不过多了一个阻塞时长,假设阻塞的线程没有被其他线程唤醒,当阻塞时长用完之后,线程就会自动解除阻塞,继续向下执行。

template <class Rep, class Period>
cv_status wait_for (unique_lock<mutex>& lck,const chrono::duration<Rep,Period>& rel_time);template <class Rep, class Period, class Predicate>
bool wait_for(unique_lock<mutex>& lck,const chrono::duration<Rep,Period>& rel_time, Predicate pred);

wait_until()函数和wait_for()的功能是一样的,它是指定让线程阻塞到某一个时间点,假设阻塞的线程没有被其他线程唤醒,当到达指定的时间点之后,线程就会自动解除阻塞,继续向下执行。

template <class Clock, class Duration>
cv_status wait_until (unique_lock<mutex>& lck,const chrono::time_point<Clock,Duration>& abs_time);template <class Clock, class Duration, class Predicate>
bool wait_until (unique_lock<mutex>& lck,const chrono::time_point<Clock,Duration>& abs_time, Predicate pred);

1.1.2 通知函数: 

void notify_one() noexcept;
void notify_all() noexcept;

1. notify_one():唤醒一个被当前条件变量阻塞的线程。
2. notify_all():唤醒全部被当前条件变量阻塞的线程 。

1.2 生产者和消费者模型:

图片基本的理解: 

生产者向任务队列中加任务,消费者从任务队列中取任务。消费者中有多个线程,生产者可以类比成老板,消费者类比成打工人。若没有任务则消费者就处于等待的状态,生产者会发通知通知消费者去取任务完成。

我们可以使用条件变量来实现一个同步队列,这个队列作为生产者线程和消费者线程的共享资源,示例代码如下:

#include <iostream>
#include <thread>
#include <mutex>
#include <list>
#include <functional>
#include <condition_variable>class SyncQueue
{
public:SyncQueue(int maxSize) : m_maxSize(maxSize) {}void put(const int& x){std::unique_lock<std::mutex> locker(m_mutex);// 判断任务队列是不是已经满了while (m_queue.size() == m_maxSize){std::cout << "任务队列已满, 请耐心等待..." << std::endl;// 阻塞线程m_notFull.wait(locker);}// 将任务放入到任务队列中m_queue.push_back(x);std::cout << x << " 被生产" << std::endl;// 通知消费者去消费m_notEmpty.notify_one();}int take(){std::unique_lock<std::mutex> locker(m_mutex);while (m_queue.empty()){std::cout << "任务队列已空,请耐心等待。。。" << std::endl;m_notEmpty.wait(locker);}// 从任务队列中取出任务(消费)int x = m_queue.front();m_queue.pop_front();// 通知生产者去生产m_notFull.notify_one();std::cout << x << " 被消费" << std::endl;return x;}bool empty(){std::lock_guard<std::mutex> locker(m_mutex);return m_queue.empty();}bool full(){std::lock_guard<std::mutex> locker(m_mutex);return m_queue.size() == m_maxSize;}int size(){std::lock_guard<std::mutex> locker(m_mutex);return m_queue.size();}private:std::list<int> m_queue;     // 存储队列数据std::mutex m_mutex;         // 互斥锁std::condition_variable m_notEmpty;   // 不为空的条件变量std::condition_variable m_notFull;    // 没有满的条件变量int m_maxSize;         // 任务队列的最大任务个数
};int main()
{SyncQueue taskQ(50);auto produce = std::bind(&SyncQueue::put, &taskQ, std::placeholders::_1);auto consume = std::bind(&SyncQueue::take, &taskQ);std::thread t1[3];std::thread t2[3];for (int i = 0; i < 3; ++i){t1[i] = std::thread(produce, i + 100);t2[i] = std::thread(consume);}for (int i = 0; i < 3; ++i){t1[i].join();t2[i].join();}return 0;
}

上述代码中函数加锁的原因是避免同时对一个任务取和放。 

由于多线程环境中线程调度的不确定性,代码的输出结果会有多种可能,这就是为什么输出结果不唯一的主要原因。

条件变量condition_variable类的wait()还有一个重载的方法,可以接受一个条件,这个条件也可以是一个返回值为布尔类型的函数,条件变量会先检查判断这个条件是否满足,如果满足条件(布尔值为true),则当前线程重新获得互斥锁的所有权,结束阻塞,继续向下执行;如果不满足条件(布尔值为false),当前线程会释放互斥锁(解锁)同时被阻塞,等待被唤醒。

上面示例程序中的put()take()函数可以做如下修改:

put()函数:

void put(const int& x)
{std::unique_lock<std::mutex> locker(m_mutex);// 根据条件阻塞线程m_notFull.wait(locker, [this]() {return m_queue.size() != m_maxSize;});// 将任务放入到任务队列中m_queue.push_back(x);std::cout << x << " 被生产" << std::endl;// 通知消费者去消费m_notEmpty.notify_one();
}

take()函数: 

int take()
{std::unique_lock<std::mutex> locker(m_mutex);m_notEmpty.wait(locker, [this]() {return !m_queue.empty();});// 从任务队列中取出任务(消费)int x = m_queue.front();m_queue.pop_front();// 通知生产者去生产m_notFull.notify_one();std::cout << x << " 被消费" << std::endl;return x;
}

修改之后可以发现,程序变得更加精简了,而且执行效率更高了,因为在这两个函数中的while循环被删掉了,但是最终的效果是一样的,推荐使用这种方式的wait()进行线程的阻塞。 

2. condition_variable_any

2.1 成员函数:

condition_variable_any的成员函数也是分为两部分:线程等待(阻塞)函数线程通知(唤醒)函数,这些函数被定义于头文件 <condition_variable>

2.1.1:等待函数:

// ①
template <class Lock> void wait (Lock& lck);
// ②
template <class Lock, class Predicate>
void wait (Lock& lck, Predicate pred);

函数①:调用该函数的线程直接被阻塞。
函数②:该函数的第二个参数是一个判断条件,是一个返回值为布尔类型的函数。
        ②.1:该参数可以传递一个有名函数的地址,也可以直接指定一个匿名函数。
        ②.2:表达式返回false当前线程被阻塞,表达式返回true当前线程不会被阻塞,继续向下执行。
可以直接传递给wait()函数的互斥锁类型有四种,分别是:
std::mutexstd::timed_mutexstd::recursive_mutexstd::recursive_timed_mutex
如果线程被该函数阻塞,这个线程会释放占有的互斥锁的所有权,当阻塞解除之后这个线程会重新得到互斥锁的所有权,继续向下执行(这个过程是在函数内部完成的,了解这个过程即可,其目的是为了避免线程的死锁)。
wait_for()函数和wait()的功能是一样的,只不过多了一个阻塞时长,假设阻塞的线程没有被其他线程唤醒,当阻塞时长用完之后,线程就会自动解除阻塞,继续向下执行。

template <class Lock, class Rep, class Period>
cv_status wait_for (Lock& lck, const chrono::duration<Rep,Period>& rel_time);template <class Lock, class Rep, class Period, class Predicate>
bool wait_for (Lock& lck, const chrono::duration<Rep,Period>& rel_time, Predicate pred);

wait_until()函数和wait_for()的功能是一样的,它是指定让线程阻塞到某一个时间点,假设阻塞的线程没有被其他线程唤醒,当到达指定的时间点之后,线程就会自动解除阻塞,继续向下执行。 

template <class Lock, class Clock, class Duration>
cv_status wait_until (Lock& lck, const chrono::time_point<Clock,Duration>& abs_time);template <class Lock, class Clock, class Duration, class Predicate>
bool wait_until (Lock& lck, const chrono::time_point<Clock,Duration>& abs_time, Predicate pred);

2.1.2 通知函数: 

void notify_one() noexcept;
void notify_all() noexcept;

1. notify_one():唤醒一个被当前条件变量阻塞的线程。
2. notify_all():唤醒全部被当前条件变量阻塞的线程 。

2.2 生产者和消费者模型:

使用条件变量condition_variable_any同样可以实现上面的生产者和消费者的例子,代码只有个别细节上有所不同:

#include <iostream>
#include <thread>
#include <mutex>
#include <list>
#include <functional>
#include <condition_variable>class SyncQueue
{
public:SyncQueue(int maxSize) : m_maxSize(maxSize) {}void put(const int& x){std::lock_guard<std::mutex> locker(m_mutex);// 根据条件阻塞线程m_notFull.wait(m_mutex, [this]() {return m_queue.size() != m_maxSize;});// 将任务放入到任务队列中m_queue.push_back(x);std::cout << x << " 被生产" << std::endl;// 通知消费者去消费m_notEmpty.notify_one();}int take(){std::lock_guard<std::mutex> locker(m_mutex);m_notEmpty.wait(m_mutex, [this]() {return !m_queue.empty();});// 从任务队列中取出任务(消费)int x = m_queue.front();m_queue.pop_front();// 通知生产者去生产m_notFull.notify_one();std::cout << x << " 被消费" << std::endl;return x;}bool empty(){std::lock_guard<std::mutex> locker(m_mutex);return m_queue.empty();}bool full(){std::lock_guard<std::mutex> locker(m_mutex);return m_queue.size() == m_maxSize;}int size(){std::lock_guard<std::mutex> locker(m_mutex);return m_queue.size();}private:std::list<int> m_queue;     // 存储队列数据std::mutex m_mutex;         // 互斥锁std::condition_variable_any m_notEmpty;   // 不为空的条件变量std::condition_variable_any m_notFull;    // 没有满的条件变量int m_maxSize;         // 任务队列的最大任务个数
};int main()
{SyncQueue taskQ(50);auto produce = std::bind(&SyncQueue::put, &taskQ, std::placeholders::_1);auto consume = std::bind(&SyncQueue::take, &taskQ);std::thread t1[3];std::thread t2[3];for (int i = 0; i < 3; ++i){t1[i] = std::thread(produce, i + 100);t2[i] = std::thread(consume);}for (int i = 0; i < 3; ++i){t1[i].join();t2[i].join();}return 0;
}

总结:以上介绍的两种条件变量各自有各自的特点,condition_variable 配合 unique_lock 使用更灵活一些,可以在任何时候自由地释放互斥锁,而condition_variable_any 如果和lock_guard 一起使用必须要等到其生命周期结束才能将互斥锁释放。但是,condition_variable_any 可以和多种互斥锁配合使用,应用场景也更广,而 condition_variable 只能和独占的非递归互斥锁(mutex)配合使用,有一定的局限性。在性能方面std::condition_variable通常提供更优的性能,因为它专门为与 std::mutex 配合使用而设计。std::condition_variable_any因其通用性可能在性能上有所折扣。这是因为它需要处理更广泛的锁类型,可能无法针对特定类型的锁进行优化。

本文参考:C++线程同步之条件变量 | 爱编程的大丙 (subingwen.cn) 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/577520.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

算法学习系列(十一):KMP算法

目录 引言一、算法概念二、题目描述三、思路讲解三、代码实现四、测试 引言 这个KMP算法就是怎么说呢&#xff0c;就是不管算法竞赛还是找工作笔试面试&#xff0c;都是非常爱问爱考的&#xff0c;其实也是因为这个算法比较难懂&#xff0c;其实就是很难&#xff0c;所以非常个…

智能优化算法应用:基于指数分布算法3D无线传感器网络(WSN)覆盖优化 - 附代码

智能优化算法应用&#xff1a;基于指数分布算法3D无线传感器网络(WSN)覆盖优化 - 附代码 文章目录 智能优化算法应用&#xff1a;基于指数分布算法3D无线传感器网络(WSN)覆盖优化 - 附代码1.无线传感网络节点模型2.覆盖数学模型及分析3.指数分布算法4.实验参数设定5.算法结果6.…

React 路由跳转

1. push 与 replace 模式 默认情况下&#xff0c;开启的是 push 模式&#xff0c;也就是说&#xff0c;每次点击跳转&#xff0c;都会向栈中压入一个新的地址&#xff0c;在点击返回时&#xff0c;可以返回到上一个打开的地址&#xff0c; 就像上图一样&#xff0c;我们每次返…

Windows下安装Oracle19C

官网下载oracle19c 以及客户端 官网地址&#xff1a;Software Download | Oracle 这个是要登录账号的,没有的可以注册,登录上 这个时候在点开这个官网:Database Software Downloads | Oracle 往下面滑 点了之后有个界面注意事项勾上,点下载,你就会下载: 安装oracle19c 解压安…

入门级:用devEco Studio创建一个鸿蒙APP

文章概叙 本文主要讲的是如何在鸿蒙的开发工具devEco Studio新建一个项目&#xff0c;全文很水&#xff0c;只适合新手!! 开始贴图 假设当前你已经下载好了devEco Studio,但是还没正式开始安装&#xff0c;此时你点击安装包&#xff0c;你会发下如下页面&#xff0c;只需要点…

【Linux系统编程】【Google面试题改编】线程之间的同步与协调 Linux文件操作

编写程序&#xff0c;有四个线程1、2、3、4 线程1的功能就是输1,线程2的功能就是输出2,以此类推……现在有四个文件ABCD初始都为空 现要让四个文件呈如下格式&#xff1a; A: 1 22 333 4444 1 22 333 4444… B: 22 333 4444 1 22 333 4444 1… C: 333 4444 1 22 333 4444 1 2…

搭建react+ant design pro+umi 项目框架

一、 写本文的原因 我搭建reactantdumi这个框架的原始资料主要是来源于&#xff08;ReactUmi4从零快速搭建中后台系统保姆级记录教程&#xff08;一、项目创建及初始化&#xff09;&#xff09; 而我写这篇文章的本意就是用来记录我用搭建时候的步骤汇总。 二、 npm和node版…

Nginx快速入门:实现企业安全防护|nginx部署https,ssl证书(七)

0. 引言 之前我们讲到nginx的一大核心作用就是实现企业安全防护&#xff0c;而实现安全防护的原理就是通过部署https证书&#xff0c;以此实现参数加密访问&#xff0c;从而加强企业网站的安全能力。 nginx作为各类服务的统一入口&#xff0c;只需要在入口处部署一个证书&…

解决Maven找不到依赖的问题

如果经过Reload Maven项目&#xff0c;清除Idea缓存&#xff0c;甚至重启Idea等方法都解决不了Dependency xxx not found的问题&#xff0c;不妨试试手动安装。 1. 进入maven仓库&#xff0c;搜索自己需要的对应版本的依赖。 2. 点击下图红框jar图标下载对应的jar包&#xff0c…

【K8S基础】-k8s的核心概念控制器和调度器

Kubernetes是一个开源的容器编排平台&#xff0c;旨在简化和自动化容器化应用程序的部署、扩展和管理。它提供了一个强大的基础设施来管理容器化应用程序的生命周期&#xff0c;并确保它们在整个集群中高效运行。 Kubernetes的核心概念包括集群、节点、Pod、控制器、调度器等。…

lv13 环境搭建之内核编译 4

一、开发板运行Linux 1. 网线连接开发板和主机 2. ubuntu下拷贝uImage、exynos4412-fs4412.dtb两个文件到/tftpboot目录下cd ~/fs4412cp uImage exynos4412-fs4412.dtb /tftpboot 3. rootfs.tar.xz解压到/opt/4412sudo tar xvf rootfs.tar.xz -C /opt/4412sudo chmod 777 /opt…

软件测试面试八股文——基础篇

5&#xff09;错误推测法&#xff1a;是基于经验和直觉推测程序中所有可能存在的各种错误&#xff0c;从而有针对性的设计测试用例的方法 6&#xff09;正交实验法 7&#xff09;判定表法 8&#xff09;测试大纲法 3、提交缺陷的八大要素 1&#xff09;缺陷编号&#xff1a…

2023.12.24 关于 Redis 中 String 类型内部编码和应用场景

目录 String 类型内部编码 3 种内部编码方式 String 类型应用场景 Cache 缓存 键名命名规则 计数&#xff08;Counter&#xff09; 共享会话&#xff08;Session &#xff09; 手机验证码 总结 String 类型内部编码 3 种内部编码方式 int&#xff1a;用来表示 64 位 —…

智能优化算法应用:基于食肉植物算法3D无线传感器网络(WSN)覆盖优化 - 附代码

智能优化算法应用&#xff1a;基于食肉植物算法3D无线传感器网络(WSN)覆盖优化 - 附代码 文章目录 智能优化算法应用&#xff1a;基于食肉植物算法3D无线传感器网络(WSN)覆盖优化 - 附代码1.无线传感网络节点模型2.覆盖数学模型及分析3.食肉植物算法4.实验参数设定5.算法结果6.…

详细学习Java注解Annotation、元注解(通俗易懂,一学就会)

概述 底层原理 自定义注解 示例代码&#xff1a; 1. 2.只有属性名为value的才可以&#xff0c;java对它进行了标识&#xff0c;如果是其他别名如value1是不行的 3.多个属性&#xff0c;必须用键值对形式&#xff0c;不能少写&#xff0c;也不能多写&#xff0c;除非有default修…

嵌入式中一文搞定C语言数据结构--跳表

大家好&#xff0c;今天分享一篇C语言数据结构相关的文章--跳表。 1. 什么是跳表 跳表是 链表 索引 的一种数据结构 &#xff0c;是以空间换取时间的方式&#xff0c;关于跳表参考: https://baike.baidu.com/item/跳表/22819833?fraladdin 2. 跳表概念 跳表在原有链表的基…

【算法】利用双指针法解决算法题(C++)

文章目录 1. 前言2. 双指针法引入283.移动零 3. 使用双指针法解决算法题1089.复写零202.快乐数11.盛最多水的容器[611.有效三角 形的个数](https://leetcode.cn/problems/valid-triangle-number/description/)LCR179.查找总价格为目标值的两个商品15.三数之和18.四数之和 1. 前…

redis基本用法学习(C#调用StackExchange.Redis操作redis)

StackExchange.Redis是基于C#的高性能通用redis操作客户端&#xff0c;也属于常用的redis客户端之一&#xff0c;本文学习其基本用法。   新建Winform项目&#xff0c;在Nuget包管理器中搜索并安装StackExchange.Redis&#xff0c;如下图所示&#xff1a;   StackExchange.…

开发利器——C语言必备实用第三方库

​ 对于广大C语言开发者来说&#xff0c;缺乏类似C STL和Boost的库会让开发受制于基础库的匮乏&#xff0c;也因此导致了开发效率的骤降。这也使得例如libevent这类事件库&#xff08;基础组件库&#xff09;一时间大红大紫。 今天&#xff0c;码哥给大家带来一款基础库&#…

JavaScript进阶(事件+获取元素+操作元素)

目录 事件基础 事件组成 执行事件的步骤 获取元素 根据ID获取元素 根据标签名获取元素 获取ol中的小li 类选择器&#xff08;html5新增的I9以上支持&#xff09; 获取body和html 操作元素 innerText和innerHtml 表单标签 样式属性操作 操作元素总结 事件基础 事…