condition_variable简介
我们知道使用互斥量std::mutex
等基本能够解决多线程之间的数据竞争机制。那么我们为什么需要条件变量,条件变量是能够在多线程环境中做什么用的呢?下面我们先来看下部分代码程序:
Demo示例代码
#include <iostream>
#include <deque>
#include <thread>
#include <mutex>
std::deque<int> Q;
std::mutex mtx;
// 向队列压入数据
void pushData()
{
int number = 50;
while (number > 0)
{
std::unique_lock<std::mutex> lck(mtx);
Q.push_front(number);
//std::this_thread::sleep_for(std::chrono::seconds(1)); / ①
lck.unlock();
std::this_thread::sleep_for(std::chrono::seconds(1)); / ②
--number;
}
}
// 从队列取出数据
void popData()
{
int data = 0;
while (data != 1)
{
std::unique_lock<std::mutex> lck(mtx);
if (!Q.empty())
{
data = Q.back();
Q.pop_back();
lck.unlock();
std::cout << "popData get value from pushData " << data << std::endl;
}
else
{
lck.unlock();
//std::this_thread::sleep_for(std::chrono::milliseconds(500));// ③ 500ms
}
}
}
int main(void)
{
std::thread push(pushData);
std::thread pop(popData);
push.join();
pop.join();
system("pause");
return 0;
}
上面这个小程序主要介绍有两个线程,一个线程往队列里面压入数据,一个线程从队列里面取出数据。这样的话就会涉及数据竞争,所以我们在压入数据的线程与取出数据的线程中分别加上互斥锁,以此来保证程序正常运行。但是,我们仔细看一下上面的这个程序,pushData()
压入数据内部解锁后需要使线程睡眠1s
中,那么在这1s
中内popData()
可能执行无数次加锁解锁的过程,但是这些加锁解锁都是没有获取有效的数据,只是判断后队列为空后直接解锁。
在这个过程中,会导致cpu占用率很高,见下图:约为15.1%。
那么,我们该如何尽可能减少cpu的占用率呢?有一种方法,就是当每次popData()
上锁后,判断队列为空后在解锁我们将此线程睡眠500ms
中后,再去看队列是否有数据。相当于一个惩罚系数,过段时间再去看看访问,以此来降低cpu的线程调度占用率。把上面代码段的③解开注释即可我们就能够发现cpu占用率降下来了,见下图:
上述让线程休眠成功降低cpu的调用占用率,但是存在一个问题:我们如何有效的设置多长时间呢?不可能依据每一次来不断的测试吧。因此,C++11
多线程中的std::condition_variable
条件变量就能够有效的解决我们上述所说的问题。
上述的程序是一旦压入数据后,队列不为空的话,那么取数据线程就可以运行。那么,我们可以使popData()
接收后唤醒处于pushData()
里面的等待线程不久可以解决这个问题了。所以,使用wait()
函数让取数据线程popData()
中如果队列没有数据,使其进行休眠。如果压入数据线程启动解锁了,使用notify_one()来通知取数据线程popData()
中的一个等待线程取消即可。这样一来,就不会出现线程之间浪费无用功的加锁解锁却不执行任何操作。代码如下:
#include <iostream>
#include <deque>
#include <thread>
#include <mutex>
std::deque<int> Q;
std::mutex mtx;
std::condition_variable cv;
void pushData()
{
int number = 100;
while (number > 0)
{
std::unique_lock<std::mutex> lck(mtx);
Q.push_front(number);
//std::this_thread::sleep_for(std::chrono::seconds(1));
lck.unlock();
cv.notify_one(); // 通知取线程解除某一个wait()等待
std::this_thread::sleep_for(std::chrono::seconds(1));
--number;
}
}
void popData()
{
int data = 0;
while (data != 1)
{
std::unique_lock<std::mutex> lck(mtx);
while (Q.empty())
cv.wait(lck); // 如果队列为空,且只需取线程,那么将进行等待之队列不为空的唤醒notify_one()
data = Q.back();
Q.pop_back();
lck.unlock();
std::cout << "popData get value from pushData " << data << std::endl;
}
}
int main(void)
{
std::thread push(pushData);
std::thread pop(popData);
push.join();
pop.join();
system("pause");
return 0;
}
上述方式使用条件变量的线程函数,能够有效降低cpu调用占用率,其次也可避免掉第一种使用线程等待时间的不确定性。下面我们上述程序的几个部分进行一些介绍与防错:
- 上述代码中的
pushData()
线程里面的等待时刻1s位置放入互斥锁里,并没有导致线程死锁,只是你在运行时刻会发现由于线程的等待会一直优先执行压入数据操作,后面会几乎统一执行取出操作。 - 无论是
pushData()
还是popData()
线程函数里面,使用的互斥锁是unique_lock
,不能使用lock_guard
。原因在于wait()
被条件变量启动时刻,会先调用互斥锁unlock()
进行解锁,然后将自己进入休眠状态。唤醒之后会继续持有锁,进行保护后面队列取出操作。我们知道lock_guard
只是在构造与析构里面包装了lock
与unlock
,并无相对应的接口,但是unique_lock
有这个接口。 - 加锁与解锁的区域范围尽量减小,提高程序的效率。
- 上述取线程函数
popData()
里面判断队列为空使用是while
循环来不断的判断,主要是避免除了notify_one()
唤醒之外的其它伪唤醒功能导致的程序错误。
条件变量std::condition_variable的成员函数
我们看下std::condition_variable
源码部分及其相对应的成员函数:
wait()
函数执行时候,首先解锁互斥量同时在本行进行阻塞,直道其它线程调用notify_one()
或者notify_all()
将其唤醒。wait()
函数第一个参数为互斥量,如果有第二个参数,那么第二个参数返回如果是false的话,那么wait()
会执行与默认只有一个参数相同的操作。如果为true的话,那么wait()
返回,继续向下执行。notify_one()
函数:通知一个线程的wait()
进行唤醒;notify_all()
函数:通知所有线程wait()
进行唤醒;wait_for()
函数:等待某一段时间内,通过cv_status
状态来进行判断;wait_until()
函数:等待至某一时刻,使用cv_status
返回状态进行判断;
enum class cv_status
{
no_timeout, // The function returned without a timeout (i.e., it was notified).
timeout // The function returned because it reached its time limit (timeout).
};
cv_status
有两种状态,超时与未超时;
class condition_variable { // class for waiting for conditions
public:
using native_handle_type = _Cnd_t;
condition_variable() { // construct
_Cnd_init_in_situ(_Mycnd());
}
~condition_variable() noexcept { // destroy
_Cnd_destroy_in_situ(_Mycnd());
}
condition_variable(const condition_variable&) = delete;
condition_variable& operator=(const condition_variable&) = delete;
void notify_one() noexcept { // wake up one waiter
_Check_C_return(_Cnd_signal(_Mycnd()));
}
void notify_all() noexcept { // wake up all waiters
_Check_C_return(_Cnd_broadcast(_Mycnd()));
}
void wait(unique_lock<mutex>& _Lck) { // wait for signal
// Nothing to do to comply with LWG 2135 because std::mutex lock/unlock are nothrow
_Check_C_return(_Cnd_wait(_Mycnd(), _Lck.mutex()->_Mymtx()));
}
template <class _Predicate>
void wait(unique_lock<mutex>& _Lck, _Predicate _Pred) { // wait for signal and test predicate
while (!_Pred()) {
wait(_Lck);
}
}
template <class _Rep, class _Period>
cv_status wait_for(unique_lock<mutex>& _Lck, const chrono::duration<_Rep, _Period>& _Rel_time) {
// wait for duration
if (_Rel_time <= chrono::duration<_Rep, _Period>::zero()) {
return cv_status::timeout;
}
// The standard says that we should use a steady clock, but unfortunately our ABI
// speaks struct xtime, which is relative to the system clock.
_CSTD xtime _Tgt;
const bool _Clamped = _To_xtime_10_day_clamped(_Tgt, _Rel_time);
const cv_status _Result = wait_until(_Lck, &_Tgt);
if (_Clamped) {
return cv_status::no_timeout;
}
return _Result;
}
template <class _Rep, class _Period, class _Predicate>
bool wait_for(unique_lock<mutex>& _Lck, const chrono::duration<_Rep, _Period>& _Rel_time, _Predicate _Pred) {
// wait for signal with timeout and check predicate
return _Wait_until1(_Lck, chrono::steady_clock::now() + _Rel_time, _Pred);
}
template <class _Clock, class _Duration>
cv_status wait_until(unique_lock<mutex>& _Lck, const chrono::time_point<_Clock, _Duration>& _Abs_time) {
// wait until time point
for (;;) {
const auto _Now = _Clock::now();
if (_Abs_time <= _Now) {
return cv_status::timeout;
}
_CSTD xtime _Tgt;
(void) _To_xtime_10_day_clamped(_Tgt, _Abs_time - _Now);
const cv_status _Result = wait_until(_Lck, &_Tgt);
if (_Result == cv_status::no_timeout) {
return cv_status::no_timeout;
}
}
}
template <class _Clock, class _Duration, class _Predicate>
bool wait_until(
unique_lock<mutex>& _Lck, const chrono::time_point<_Clock, _Duration>& _Abs_time, _Predicate _Pred) {
// wait for signal with timeout and check predicate
return _Wait_until1(_Lck, _Abs_time, _Pred);
}
cv_status wait_until(unique_lock<mutex>& _Lck, const xtime* _Abs_time) {
// wait for signal with timeout
if (!_Mtx_current_owns(_Lck.mutex()->_Mymtx())) {
_Throw_Cpp_error(_OPERATION_NOT_PERMITTED);
}
// Nothing to do to comply with LWG 2135 because std::mutex lock/unlock are nothrow
const int _Res = _Cnd_timedwait(_Mycnd(), _Lck.mutex()->_Mymtx(), _Abs_time);
switch (_Res) {
case _Thrd_success:
return cv_status::no_timeout;
case _Thrd_timedout:
return cv_status::timeout;
default:
_Throw_C_error(_Res);
}
}
template <class _Predicate>
bool wait_until(unique_lock<mutex>& _Lck, const xtime* _Abs_time, _Predicate _Pred) {
// wait for signal with timeout and check predicate
return _Wait_until1(_Lck, _Abs_time, _Pred);
}
_NODISCARD native_handle_type native_handle() { // return condition variable handle
return _Mycnd();
}
void _Register(unique_lock<mutex>& _Lck, int* _Ready) { // register this object for release at thread exit
_Cnd_register_at_thread_exit(_Mycnd(), _Lck.release()->_Mymtx(), _Ready);
}
void _Unregister(mutex& _Mtx) { // unregister this object for release at thread exit
_Cnd_unregister_at_thread_exit(_Mtx._Mymtx());
}
private:
aligned_storage_t<_Cnd_internal_imp_size, _Cnd_internal_imp_alignment> _Cnd_storage;
_Cnd_t _Mycnd() noexcept { // get pointer to _Cnd_internal_imp_t inside _Cnd_storage
return reinterpret_cast<_Cnd_t>(&_Cnd_storage);
}
template <class _Predicate>
bool _Wait_until1(unique_lock<mutex>& _Lck, const xtime* _Abs_time, _Predicate& _Pred) {
// wait for signal with timeout and check predicate
while (!_Pred()) {
if (wait_until(_Lck, _Abs_time) == cv_status::timeout) {
return _Pred();
}
}
return true;
}
template <class _Clock, class _Duration, class _Predicate>
bool _Wait_until1(
unique_lock<mutex>& _Lck, const chrono::time_point<_Clock, _Duration>& _Abs_time, _Predicate& _Pred) {
while (!_Pred()) {
const auto _Now = _Clock::now();
if (_Abs_time <= _Now) {
return false;
}
_CSTD xtime _Tgt;
const bool _Clamped = _To_xtime_10_day_clamped(_Tgt, _Abs_time - _Now);
if (wait_until(_Lck, &_Tgt) == cv_status::timeout && !_Clamped) {
return _Pred();
}
}
return true;
}
};
小结
多线程编程需要实现一个主要目的在于线程间的协同工作来提升效率。所以,线程之间的通信机制十分重要,来告诉消息的互通有无。C++11多线程可以通过条件变量wait()来进行等待某个事件发生,当其它线程准备好相关数据后,可以通过notify来唤醒正在等待的线程。这样能够有效降低线程之间的频繁加锁解锁却无用功的执行。