C++11 新特性之条件变量

       本文是C++11新特性介绍的第十五部分,涉及到条件变量(condition_variable)相关的新特性。

condition_variable

介绍

       在C++11中,我们可以使用条件变量(condition_variable)实现多个线程间的同步操作;当条件不满足时,相关线程被一直阻塞,直到某种条件出现,这些线程才会被唤醒。
       其主要成员函数如下:
demo
       条件变量是利用线程间共享的全局变量进行同步的一种机制,主要包括两个动作:
       1.一个线程因等待“条件变量的条件成立”而挂起;
       2.另外一个线程使“条件成立”,给出信号,从而唤醒被等待的线程。
       为了防止竞争,条件变量的使用总是和一个互斥锁结合在一起;通常情况下这个锁是std::mutex,并且管理这个锁只能是 std::unique_lockstd::mutex RAII模板类。
       上面提到的两个步骤,分别是使用以下两个方法实现:
       1.等待条件成立使用的是condition_variable类成员wait 、wait_for 或 wait_until。
       2.给出信号使用的是condition_variable类成员notify_one或者notify_all函数。

细节说明

       在条件变量中只能使用std::unique_lock。unique_lock和lock_guard都是管理锁的辅助类工具,都是RAII风格;它们是在定义时获得锁,在析构时释放锁。它们的主要区别在于unique_lock锁机制更加灵活,可以再需要的时候进行lock或者unlock调用,不非得是析构或者构造时,它们的区别可以通过成员函数就可以一目了然。
       线程的阻塞是通过成员函数wait()/wait_for()/wait_until()函数实现的。这里主要说明前面两个函数:

wait成员函数

       函数声明如下:

1
2
3
4
void wait(std::unique_lock<std::mutex>& lock);
//Predicate 谓词函数,可以普通函数或者lambda表达式
templateclass Predicate >
void wait(std::unique_lock<std::mutex>& lock, Predicate pred);

       wait 导致当前线程阻塞直至条件变量被通知,或虚假唤醒发生,可选地循环直至满足某谓词。

wait_for成员函数

       函数声明如下:

1
2
3
4
5
6
7
8
templateclass Rep, class Period >
std::cv_status wait_for(std::unique_lock<std::mutex>& lock,
                        const std::chrono::duration<Rep, Period>& rel_time);
templateclass Rep, class Period, class Predicate >
bool wait_for(std::unique_lock<std::mutex>& lock,
              const std::chrono::duration<Rep, Period>& rel_time,
              Predicate pred);

       wait_for 导致当前线程阻塞直至条件变量被通知,或虚假唤醒发生,或者超时返回。
       返回值说明:
       1.若经过 rel_time 所指定的关联时限则为 std::cv_status::timeout,否则为 std::cv_status::no_timeout。
       2.若经过 rel_time 时限后谓词 pred 仍求值为 false 则为 false ,否则为 true 。
       以上两个类型的wait函数都在会阻塞时,自动释放锁权限,即调用unique_lock的成员函数unlock(),以便其他线程能有机会获得锁。这就是条件变量只能和unique_lock一起使用的原因,否则当前线程一直占有锁,线程被阻塞。

notify_all/notify_one

       notify函数声明如下:

1
void notify_one() noexcept;

       若任何线程在 *this 上等待,则调用 notify_one 会解阻塞(唤醒)等待线程之一。

1
void notify_all() noexcept;

       若任何线程在 *this 上等待,则解阻塞(唤醒)全部等待线程。

虚假唤醒

       在正常情况下,wait类型函数返回时要不是因为被唤醒,要不是因为超时才返回,但是在实际中发现,因此操作系统的原因,wait类型在不满足条件时,它也会返回,这就导致了虚假唤醒。因此,我们一般都是使用带有谓词参数的wait函数,因为这种(xxx, Predicate pred)类型的函数等价于:

1
2
3
4
while(!pred()) //while循环,解决了虚假唤醒的问题
{
    wait(lock);
}

       原因说明如下,假设系统不存在虚假唤醒的时,代码形式如下:

1
2
3
4
5
6
7
8
9
10
if(不满足xxx条件)
{
    //没有虚假唤醒,wait函数可以一直等待,直到被唤醒或者超时,没有问题。
    //但实际中却存在虚假唤醒,导致假设不成立,wait不会继续等待,跳出if语句,
    //提前执行其他代码,流程异常
    wait();  
}
//其他代码
...

       正确的使用方式,使用while语句解决:

1
2
3
4
5
6
7
8
while(!(xxx条件))
{
    //虚假唤醒发生,由于while循环,再次检查条件是否满足,
    //否则继续等待,解决虚假唤醒
    wait();  
}
//其他代码
....

条件变量使用

       在这里,我们使用条件变量,解决生产者-消费者问题,该问题主要描述如下:
       生产者-消费者问题,也称有限缓冲问题,是一个多进程/线程同步问题的经典案例。该问题描述了共享固定大小缓冲区的两个进程/线程——即所谓的“生产者”和“消费者”,在实际运行时会发生的问题。
       生产者的主要作用是生成一定量的数据放到缓冲区中,然后重复此过程。与此同时,费者也在缓冲区消耗这些数据。该问题的关键就是要保证生产者不会在缓冲区满时加入数据,消费者也不会在缓冲区中空时消耗数据。
       要解决该问题,就必须让生产者在缓冲区满时休眠(要么干脆就放弃数据),等到下次消费者消耗缓冲区中的数据的时候,生产者才能被唤醒,开始往缓冲区添加数据。
       同样,也可以让消费者在缓冲区空时进入休眠,等到生产者往缓冲区添加数据之后,再唤醒消费者。
       生产者-消费者代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
#include <thread>
#include <iostream>
std::mutex g_cvMutex;
std::condition_variable g_cv;
//缓存区
std::deque<int> g_data_deque;
//缓存区最大数目
const int MAX_NUM = 30;
//数据
int g_next_index = 0;
//生产者,消费者线程个数
const int PRODUCER_THREAD_NUM = 3;
const int CONSUMER_THREAD_NUM = 3;
void producer_thread(int thread_id)
{
while(true)
{
std::this_thread::sleep_for(std::chrono::milliseconds(500));
//加锁
std::unique_lock <std::mutex> lk(g_cvMutex);
//当队列未满时,继续添加数据
g_cv.wait(lk, [](){ return g_data_deque.size() <= MAX_NUM; });
g_next_index++;
g_data_deque.push_back(g_next_index);
std::cout << "producer_thread: " << thread_id << " producer data: " << g_next_index;
std::cout << " queue size: " << g_data_deque.size() << std::endl;
//唤醒其他线程
g_cv.notify_all();
//自动释放锁
}
}
void consumer_thread(int thread_id)
{
while(true)
{
std::this_thread::sleep_for(std::chrono::milliseconds(550));
//加锁
std::unique_lock <std::mutex> lk(g_cvMutex);
//检测条件是否达成
g_cv.wait( lk, []{ return !g_data_deque.empty(); });
//互斥操作,消息数据
int data = g_data_deque.front();
g_data_deque.pop_front();
std::cout << "consumer_thread: " << thread_id << " consumer data: ";
std::cout << data << " deque size: " << g_data_deque.size() << std::endl;
//唤醒其他线程
g_cv.notify_all();
//自动释放锁
}
}
int main()
{
std::thread arrRroducerThread[PRODUCER_THREAD_NUM];
std::thread arrConsumerThread[CONSUMER_THREAD_NUM];
for(int i = 0; i < PRODUCER_THREAD_NUM; i++)
{
arrRroducerThread[i] = std::thread(producer_thread, i);
}
for(int i = 0; i < CONSUMER_THREAD_NUM; i++)
{
arrConsumerThread[i] = std::thread(consumer_thread, i);
}
for(int i = 0; i < PRODUCER_THREAD_NUM; i++)
{
arrRroducerThread[i].join();
}
for(int i = 0; i < CONSUMER_THREAD_NUM; i++)
{
arrConsumerThread[i].join();
}
return 0;
}

       运行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
producer_thread: 0 producer data: 1 queue size: 1
producer_thread: 1 producer data: 2 queue size: 2
producer_thread: 2 producer data: 3 queue size: 3
consumer_thread: 1 consumer data: 1 deque size: 2
consumer_thread: 0 consumer data: 2 deque size: 1
consumer_thread: 2 consumer data: 3 deque size: 0
producer_thread: 1 producer data: 4 queue size: 1
producer_thread: 2 producer data: 5 queue size: 2
producer_thread: 0 producer data: 6 queue size: 3
consumer_thread: 0 consumer data: 4 deque size: 2
consumer_thread: 2 consumer data: 5 deque size: 1
consumer_thread: 1 consumer data: 6 deque size: 0
producer_thread: 1 producer data: 7 queue size: 1
producer_thread: 2 producer data: 8 queue size: 2
producer_thread: 0 producer data: 9 queue size: 3
consumer_thread: 1 consumer data: 7 deque size: 2
consumer_thread: 2 consumer data: 8 deque size: 1
consumer_thread: 0 consumer data: 9 deque size: 0
producer_thread: 2 producer data: 10 queue size: 1
producer_thread: 0 producer data: 11 queue size: 2
producer_thread: 1 producer data: 12 queue size: 3
....

文章目录
  1. 1. condition_variable
    1. 1.1. 介绍
    2. 1.2. 细节说明
    3. 1.3. wait成员函数
    4. 1.4. wait_for成员函数
    5. 1.5. notify_all/notify_one
    6. 1.6. 虚假唤醒
    7. 1.7. 条件变量使用