条件变量是 C++11 提供的另外一种用于等待的同步机制,它能阻塞一个或多个线程,直到收到另外一个线程发出的通知或者超时时,才会唤醒当前阻塞的线程。条件变量需要和互斥量配合起来使用,C++11 提供了两种条件变量:
condition_variable
:需要配合 std::unique_lockstd::mutex
进行 wait 操作,也就是阻塞线程的操作。condition_variable_any
:可以和任意带有 lock( )、unlock( )
语义的 mutex 搭配使用,也就是说有四种:
条件变量通常用于生产者和消费者模型,大致使用过程如下:
notify_one( )
或者 notify_all( )
唤醒一个或者所有被阻塞的线程
成员函数:
condition_variable
的成员函数主要分为两部分:线程等待(阻塞)函数 和线程通知(唤醒)函数,这些函数被定义于头文件
**等待函数:**wait()
调用 wait() 函数的线程会被阻塞
// ①
void wait (unique_lock<mutex>& lck);
// ②
template <class Predicate>
void wait (unique_lock<mutex>& lck, Predicate pred);
wait_for()
wait_for()
函数和 wait() 的功能是一样的,只不过多了一个阻塞时长,假设阻塞的线程没有被其他线程唤醒,当阻塞时长用完之后,线程就会自动解除阻塞,继续向下执行。
cv_status wait_for (unique_lock<mutex>& lck, const chrono::duration<Rep,Period>& rel_time);
bool wait_for(unique_lock<mutex>& lck, const chrono::duration<Rep,Period>& rel_time, Predicate pred);
wait_until()
cv_status wait_until (unique_lock<mutex>& lck, const chrono::time_point<Clock,Duration>& abs_time);
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;
我们可以使用条件变量来实现一个同步队列,这个队列作为生产者线程和消费者线程的共享资源,示例代码如下:
#include
#include
#include
#include
#include
#include
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]() { //注意 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
类似
总结:以上介绍的两种互斥锁各自有各自的特点,condition_variable
配合 unique_lock
使用更灵活一些,可以在在任何时候自由地释放互斥锁,而 condition_variable_any
如果和 lock_guard
一起使用必须要等到其生命周期结束才能将互斥锁释放。
但是,condition_variable_any
可以和多种互斥锁配合使用,应用场景也更广,而 condition_variable
只能和独占的非递归互斥锁(mutex)配合使用,有一定的局限性。