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

一、什么是条件变量?

条件变量类(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 线程阻塞有两种情况,一个是休眠,另一个是挂起。休眠是在特时刻恢复运行,挂起是等待某个条件后恢复运行。休眠不知道继续执行的具体时间,挂起则切确知道回复运行的时刻。

  • 3
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
boost中的variables_map是一个用于存储和访问命令行参数的数据结构。它是boost::program_options的一部分。 variables_map可以用来解析命令行参数,并将其存储为键值对的形式。它允许你通过键访问命令行参数的值,或者检查是否存在某个特定的键。 以下是一个简单的示例,展示了如何使用variables_map来解析命令行参数: ```cpp #include <boost/program_options.hpp> #include <iostream> int main(int argc, char** argv) { namespace po = boost::program_options; // 创建variables_map对象 po::variables_map vm; // 定义命令行参数选项 po::options_description desc("Allowed options"); desc.add_options() ("help", "produce help message") ("input", po::value<std::string>(), "input file") ("output", po::value<std::string>(), "output file"); // 解析命令行参数 po::store(po::parse_command_line(argc, argv, desc), vm); po::notify(vm); // 检查是否存在某个特定的键 if (vm.count("help")) { std::cout << desc << std::endl; return 0; } // 访问命令行参数的值 if (vm.count("input")) { std::cout << "Input file: " << vm["input"].as<std::string>() << std::endl; } if (vm.count("output")) { std::cout << "Output file: " << vm["output"].as<std::string>() << std::endl; } return 0; } ``` 在上述示例中,我们首先创建了一个variables_map对象vm。然后,我们使用options_description定义了三个命令行参数选项:help、input和output。其中,help选项不接受任何值,而input和output选项接受一个字符串值。接下来,我们使用parse_command_line函数解析命令行参数,并将结果存储在vm中。最后,我们使用count和as函数来访问和打印命令行参数的值。 希望这能解答你的问题!如果有任何疑问,请随时问我。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值