【C++入门到精通】condition_variable(条件变量)C++11 [ C++入门 ]

简介: 【C++入门到精通】condition_variable(条件变量)C++11 [ C++入门 ]

引言

当谈到多线程编程时,同步操作是一个不可忽视的问题。为了实现线程之间的协调和通信,C++11引入了一组非常强大的同步原语,其中之一就是condition_variable(条件变量)。在本文中,我们将深入探讨condition_variable的使用方法和原理,我们将学习如何使用condition_variable来实现线程的等待和唤醒机制。让我们开始深入研究condition_variable吧!无论您是想了解更多关于线程同步的内容,还是希望提高自己在多线程编程方面的能力,本文都将为您提供有价值的知识和实际应用技巧

一、condition_variable简介

1. 官方文档

⭕condition_variable官方文档

2. 概念简介

condition_variable是C++11引入的一个同步原语,用于实现线程之间的等待和唤醒机制。它是一种条件变量,可以与mutex(互斥锁)结合使用,实现复杂的线程同步和通信。

condition_variable的主要作用是允许一个或多个线程等待某个条件满足后再继续执行。在等待期间,线程会被阻塞,不会消耗CPU资源,直到其他线程通过通知(notify)来唤醒它们。

二、成员函数

1. wait() 函数

  • 头文件
    wait函数是condition_variable类的成员函数,它的声明位于<condition_variable>头文件中。在使用wait函数时,需要包含该头文件。
  • 函数原型
  • wait函数的原型如下:
void wait(unique_lock<mutex>& lock);

wait函数接受一个unique_lock<mutex>类型的引用作为参数。unique_lock是一个互斥锁的封装类,可以提供更灵活的加锁和解锁操作。

  • 参数解释
  • wait函数的参数是一个unique_lock类型的引用,用于保护共享数据结构。在调用wait函数之前,需要先获取该互斥锁。wait函数会将当前线程阻塞,并且会自动释放这个互斥锁。

2. notify_one() 函数

  • 头文件
  • 它的声明位于<condition_variable>头文件中。在使用notify_one函数时,需要包含该头文件。
  • 函数原型
    notify_one函数的原型如下:
void notify_one() noexcept;

notify_one函数没有参数,且是一个noexcept函数。

3.notify_all() 函数

notify_all函数是condition_variable类的成员函数,用于唤醒所有等待中的线程。

  • 头文件
    notify_all函数的声明位于<condition_variable>头文件中。在使用notify_all函数时,需要包含该头文件。
  • 函数原型
    notify_all函数的原型如下:
void notify_all() noexcept;

notify_all函数没有参数,且是一个noexcept函数。

三、使用示例

· 使用条件变量实现生产者 —— 消费者模型

#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <queue>

std::queue<int> g_queue;
std::mutex g_mutex;
std::condition_variable g_cv;

void producer() {
    for (int i = 0; i < 10; ++i) {
        std::this_thread::sleep_for(std::chrono::seconds(1));

        std::unique_lock<std::mutex> lock(g_mutex);
        g_queue.push(i);

        std::cout << "Producer: " << i << std::endl;

        // 通知等待中的消费者线程
        g_cv.notify_one();
    }
}

void consumer(int id) {
    for (int i = 0; i < 5; ++i) {
        std::unique_lock<std::mutex> lock(g_mutex);

        // 等待条件满足
        g_cv.wait(lock, [] { return !g_queue.empty(); });

        int value = g_queue.front();
        g_queue.pop();

        std::cout << "Consumer " << id << ": " << value << std::endl;
    }
}

int main() {
    std::thread t1(producer);
    std::thread t2(consumer, 1);
    std::thread t3(consumer, 2);

    t1.join();
    t2.join();
    t3.join();

    return 0;
}

在这个示例中,我们创建了一个全局队列 g_queue,一个互斥锁 g_mutex 和一个条件变量 g_cv。生产者线程通过循环将数字放入队列中,并使用 notify_one() 通知等待中的消费者线程。消费者线程在循环中等待条件满足,并使用 wait() 在等待时释放互斥锁,直到收到生产者线程的通知后再次获得互斥锁。


你可以运行这个示例,观察生产者和消费者之间的交互。生产者每秒生产一个数字,并输出到屏幕上;消费者每次从队列中取出一个数字,并输出到屏幕上。注意,消费者线程可能在某些时刻会同时被唤醒,但只有一个消费者能够获取到互斥锁并处理数据

· 两个线程交替打印,一个打印奇数,一个打印偶数

下面的代码实现了一个使用两个线程交替打印奇数和偶数的功能。在主函数中调用了 two_thread_print() 函数来启动两个线程。

其中,线程 t1 负责打印偶数,线程 t2 负责打印奇数。通过互斥锁 mtx 和条件变量 c 来实现线程之间的同步和通信。变量 flag 用于控制线程打印奇数还是偶数的判断。

具体流程如下:

  • 线程 t1 循环打印偶数,每次打印完后将 flag 设置为 false,并调用 c.notify_one() 通知线程 t2
  • 线程 t2 循环打印奇数,每次打印完后将 flag 设置为 true,并调用 c.notify_one() 通知线程 t1

这样,两个线程就可以交替执行,按顺序打印出奇数和偶数。

#include <thread>
#include <mutex>
#include <condition_variable>

void two_thread_print()
{
    std::mutex mtx;                 // 创建一个互斥锁
    std::condition_variable c;      // 创建一个条件变量
    int n = 100;                    // 打印的最大数值
    bool flag = true;               // 初始时打印偶数

    // 创建线程 t1,用于打印偶数
    std::thread t1([&](){
        int i = 0;
        while (i < n)
        {
            std::unique_lock<std::mutex> lock(mtx);   // 加锁,必须使用 unique_lock
            c.wait(lock, [&]()->bool{return flag; }); // 等待 flag 为 true
            std::cout << i << std::endl;              // 打印偶数
            flag = false;                             // 设置 flag 为 false,表示该轮打印奇数
            i += 2;                                   // 增加偶数计数器
            c.notify_one();                           // 通知线程 t2
        }
    });

    // 创建线程 t2,用于打印奇数
    std::thread t2([&](){
        int j = 1;
        while (j < n)
        {
            std::unique_lock<std::mutex> lock(mtx);   // 加锁,必须使用 unique_lock
            c.wait(lock, [&]()->bool{return !flag; });// 等待 flag 为 false
            std::cout << j << std::endl;              // 打印奇数
            flag = true;                              // 设置 flag 为 true,表示该轮打印偶数
            j += 2;                                   // 增加奇数计数器
            c.notify_one();                           // 通知线程 t1
        }
    });

    // 等待两个线程执行完毕
    t1.join();
    t2.join();
}

int main()
{
    two_thread_print(); // 启动两个线程交替打印奇数和偶数

    return 0;
}

温馨提示

感谢您对博主文章的关注与支持!另外,我计划在未来的更新中持续探讨与本文相关的内容,会为您带来更多关于C++以及编程技术问题的深入解析、应用案例和趣味玩法等。请继续关注博主的更新,不要错过任何精彩内容!

再次感谢您的支持和关注。期待与您建立更紧密的互动,共同探索C++、算法和编程的奥秘。祝您生活愉快,排便顺畅!


目录
相关文章
|
2月前
|
编译器 C++
C++入门12——详解多态1
C++入门12——详解多态1
47 2
C++入门12——详解多态1
|
2月前
|
C++
C++入门13——详解多态2
C++入门13——详解多态2
89 1
|
2月前
|
存储 安全 编译器
【C++打怪之路Lv1】-- 入门二级
【C++打怪之路Lv1】-- 入门二级
32 0
|
2月前
|
自然语言处理 编译器 C语言
【C++打怪之路Lv1】-- C++开篇(入门)
【C++打怪之路Lv1】-- C++开篇(入门)
39 0
|
2月前
|
分布式计算 Java 编译器
【C++入门(下)】—— 我与C++的不解之缘(二)
【C++入门(下)】—— 我与C++的不解之缘(二)
|
2月前
|
编译器 Linux C语言
【C++入门(上)】—— 我与C++的不解之缘(一)
【C++入门(上)】—— 我与C++的不解之缘(一)
|
2月前
|
编译器 C++
C++入门11——详解C++继承(菱形继承与虚拟继承)-2
C++入门11——详解C++继承(菱形继承与虚拟继承)-2
41 0
|
1月前
|
存储 编译器 C语言
【c++丨STL】string类的使用
本文介绍了C++中`string`类的基本概念及其主要接口。`string`类在C++标准库中扮演着重要角色,它提供了比C语言中字符串处理函数更丰富、安全和便捷的功能。文章详细讲解了`string`类的构造函数、赋值运算符、容量管理接口、元素访问及遍历方法、字符串修改操作、字符串运算接口、常量成员和非成员函数等内容。通过实例演示了如何使用这些接口进行字符串的创建、修改、查找和比较等操作,帮助读者更好地理解和掌握`string`类的应用。
51 2
|
1月前
|
存储 编译器 C++
【c++】类和对象(下)(取地址运算符重载、深究构造函数、类型转换、static修饰成员、友元、内部类、匿名对象)
本文介绍了C++中类和对象的高级特性,包括取地址运算符重载、构造函数的初始化列表、类型转换、static修饰成员、友元、内部类及匿名对象等内容。文章详细解释了每个概念的使用方法和注意事项,帮助读者深入了解C++面向对象编程的核心机制。
107 5
|
1月前
|
存储 编译器 C++
【c++】类和对象(中)(构造函数、析构函数、拷贝构造、赋值重载)
本文深入探讨了C++类的默认成员函数,包括构造函数、析构函数、拷贝构造函数和赋值重载。构造函数用于对象的初始化,析构函数用于对象销毁时的资源清理,拷贝构造函数用于对象的拷贝,赋值重载用于已存在对象的赋值。文章详细介绍了每个函数的特点、使用方法及注意事项,并提供了代码示例。这些默认成员函数确保了资源的正确管理和对象状态的维护。
98 4