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

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

  • condition_variable
    • 成员函数
    • 生产者和消费者模型
  • condition_variable_any
    • 成员函数
    • 生产者和消费者模型

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

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

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

  1. 拥有条件变量的线程获取互斥量

  2. 循环检查某个条件,如果条件不满足阻塞当前线程,否则线程继续向下执行

    产品的数量达到上限,生产者阻塞,否则生产者一直生产。
    产品的数量为零,消费者阻塞,否则消费者一直消费。

  3. 条件满足之后,可以调用notify_one()或者notify_all()唤醒一个或者所有被阻塞的线程

    由消费者唤醒被阻塞的生产者,生产者解除阻塞继续生产。
    由生产者唤醒被阻塞的消费者,消费者解除阻塞继续消费。

condition_variable

成员函数

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

// ①
void wait (unique_lock<mutex>& lck);
// ②
template <class Predicate>
void wait (unique_lock<mutex>& lck, Predicate pred);
  • 函数①:调用该函数的线程直接被阻塞

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

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

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

在这里插入图片描述

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

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);

通知函数

void notify_one() noexcept;
void notify_all() noexcept;
  • notify_one():唤醒一个被当前条件变量阻塞的线程
  • notify_all():唤醒全部被当前条件变量阻塞的线程

生产者和消费者模型

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

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

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

condition_variable_any

成员函数

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

  • 等待函数
// ①
template <class Lock> void wait (Lock& lck);
// ②
template <class Lock, class Predicate>
void wait (Lock& lck, Predicate pred);
  • 函数①:调用该函数的线程直接被阻塞

  • 函数②:该函数的第二个参数是一个判断条件,是一个返回值为布尔类型的函数
    该参数可以传递一个有名函数的地址,也可以直接指定一个匿名函数
    表达式返回false当前线程被阻塞,表达式返回true当前线程不会被阻塞,继续向下执行

  • 可以直接传递给wait()函数的互斥锁类型有四种,分别是:
    std::mutex、std::timed_mutex、std::recursive_mutex、std::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);
  • 通知函数
void notify_one() noexcept;
void notify_all() noexcept;
  • notify_one():唤醒一个被当前条件变量阻塞的线程
  • notify_all():唤醒全部被当前条件变量阻塞的线程

生产者和消费者模型

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

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

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

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

相关文章

ROS 2基础概念#5:执行器(Executor)| ROS 2学习笔记

在ROS 2中&#xff0c;Executor是一个核心概念&#xff0c;负责管理节点&#xff08;Node&#xff09;中的回调函数&#xff0c;如订阅消息的回调、服务请求的回调、定时器回调等。Executor决定了何时以及如何执行这些回调&#xff0c;从而在ROS 2系统中实现异步编程。 ROS 2 …

vscode自定义插件的开发过程记录

前言 本文是关于visual studio code软件上自定义插件的开发记录&#xff0c;将从头记录本人开发的过程&#xff0c;虽然网上也有很多文章&#xff0c;但个人在实践的过程还是会遇到不一样的问题&#xff0c;所以记录下来&#xff0c;以便于后期参考。 前期准备&#xff1a; 1、…

STL之list容器代码详解

1 基础概念 功能&#xff1a; 将数据进行链式存储 链表&#xff08;list&#xff09;是一种物理存储单元上非连续的存储结构&#xff0c;数据元素的逻辑顺序是通过链表中的指针链接实现的 链表的组成&#xff1a;链表由一系列结点组成。 结点的组成&#xff1a;一个是存储数…

嵌入式学习第二十五天!(网络的概念、UDP编程)

网络&#xff1a; 可以用来&#xff1a;数据传输、数据共享 1. 网络协议模型&#xff1a; 1. OSI协议模型&#xff1a; 应用层实际收发的数据表示层发送的数据是否加密会话层是否建立会话连接传输层数据传输的方式&#xff08;数据包&#xff0c;流式&#xff09;网络层数据的…

HelpLook VS GitBook:知识库优劣详解

在信息爆炸的时代&#xff0c;企业要保持竞争优势&#xff0c;就必须善于管理和利用内部的知识资产。企业知识库作为一种集中存储和共享知识的工具&#xff0c;正在成为现代企业不可或缺的一部分。 HelpLook和Gitbook是提供专业知识库的两个平台&#xff0c;也被大众熟知。它们…

一文读懂HDMI的演变-从HDMI1.0到HDMI2.1(建议收藏)

HDMI&#xff0c;全称为&#xff08;High Definition Multimedia Interface&#xff09;高清多媒体接口&#xff0c;主要用于传输高清音视频信号。 HDMI System HDMI系统包括HDMI的source和HDMI的sink, 其中source 是源端&#xff0c;即信号的来源&#xff1b;Sink的接收端&a…

什么是以人为本的AI?

AI技术蓬勃发展&#xff0c;有望极大改善我们的日常生活。因此&#xff0c;人工智能专家经常围绕在我们社会中利用人工智能的最佳方式展开对话&#xff0c;并由此得出了以人为中心的AI方法。以人为中心的AI意为不是用机器代替人类&#xff0c;而是用机器来优化人类的体验。 在…

乌鸡的身高

解法&#xff1a; 只需要看身高最高的乌鸡个数是否>2.若满足则除去当前这只乌鸡的最高身高都是最高身高。 若不满足则只需要看最高的和第二高的乌鸡。 #include<iostream> #include<vector> #include<algorithm> #include<cmath> using namespac…

图像检索与识别——词袋模型(Bag-of-features models)

一、定义 计算机视觉单词袋是一种描述计算图像之间相似度的技术&#xff0c;常用于用于图像分类当中。该方法起源于文本检索&#xff08;信息检索&#xff09;&#xff0c;是对NLP“单词袋”算法的扩展。在“单词袋”中&#xff0c;我们扫描整个文档&#xff0c;并保留文档中出…

STL中push_back和emplace_back效率的对比

文章目录 过程对比1.通过构造参数向vector中插入对象&#xff08;emplace_back更高效&#xff09;2.通过插入实例对象&#xff08;调用copy函数&#xff09;3.通过插入临时对象&#xff08;调用move函数&#xff09; 效率对比emplace_back 的缺点 我们以STL中的vector容器为例。…

LeetCode 刷题 [C++] 第300题.最长递增子序列

题目描述 给你一个整数数组 nums &#xff0c;找到其中最长严格递增子序列的长度。 子序列 是由数组派生而来的序列&#xff0c;删除&#xff08;或不删除&#xff09;数组中的元素而不改变其余元素的顺序。例如&#xff0c;[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。 题目…

Leetcode 206. 反转链表

给你单链表的头节点 head &#xff0c;请你反转链表&#xff0c;并返回反转后的链表。 示例 1&#xff1a; 输入&#xff1a;head [1,2,3,4,5] 输出&#xff1a;[5,4,3,2,1] 示例 2&#xff1a; 输入&#xff1a;head [1,2] 输出&#xff1a;[2,1] 示例 3&#xff1a; 输…