条件变量是C++11提供的另外一种用于等待的同步机制,它能阻塞一个或多个线程,直到收到另外一个线程发出的通知或者超时,才会唤醒当前阻塞的线程。条件变量需要和互斥量配合起来用。C++11提供了两种条件变量:
1、condition_variable,配合std::unique_lock
2、condition_variable_any,和任意带有lock,unlokc语义的mutex搭配使用,比较灵活,但效率比condition_variable差一些。
可以看到condition_variable_any比condition_variable更灵活,因为它更通用,对所有的锁都适用,而condition_variable性能更好。
条件变量的使用过程如下:
1、拥有条件变量的线程获取互斥量。
2、循环检查某个条件,如果条件不满足,则阻塞直到条件满足;如果条件满足,则向下执行。
3、某个线程满足条件执行完之后调用notify_one或notify_all唤醒一个或者所有的等待线程。
可以用条件变量来实现一个同步队列,同步队列作为一个线程安全的数据共享区,经常用于线程之间数据读取,比如半同步半异步线程池的同步队列。
#include
#include
#include
#include
#include
#include
using namespace std;
template
class SyncQueue
{
bool IsFull() const
{
return m_queue.size() == m_maxSize;
}
bool IsEmpty() const
{
return m_queue.empty();
}
public:
SyncQueue(int maxSize) : m_maxSize(maxSize){}
void Put(const T& x)
{
std::lock_guard locker(m_mutex);
while(IsFull())
{
cout << "缓冲区满了,需要等待..." << endl;
m_notFull.wait(m_mutex);
}
m_queue.push_back(x);
m_notEmpty.notify_one();
}
void Take(T& x)
{
std::lock_guard locker(m_mutex);
while(IsEmpty())
{
cout << "缓冲区空了,需要等待..." << endl;
m_notEmpty.wait(m_mutex);
}
x = m_queue.front();
m_queue.pop_front();
m_notFull.notify_one();
}
int Count()
{
return m_queue.size();
}
bool Empty()
{
std::lock_guard locker(m_mutex);
return m_queue.empty();
}
bool Full()
{
std::lock_guard locker(m_mutex);
return m_queue.size() == m_maxSize;
}
private:
std::list m_queue; ///缓冲区
std::mutex m_mutex; ///互斥量和条件变量结合起来使用
std::condition_variable_any m_notEmpty; ///不为空的条件变量
std::condition_variable_any m_notFull; ///没有满的条件变量
int m_maxSize; ///同步队列最大的size
};
SyncQueue myqueue(1024);
void send_thr()
{
int i = 0;
char strbuf[100] = {0};
std::chrono::milliseconds sleepDuration(2000);
while(1)
{
snprintf(strbuf, sizeof(strbuf), "send %d", ++i);
myqueue.Put(strbuf);
std::this_thread::sleep_for(sleepDuration);
if (i > 1000)
{
i = 0;
}
}
}
void recv_thr()
{
std::string str;
std::chrono::milliseconds sleepDuration(1000);
while(1)
{
myqueue.Take(str);
cout << str << endl;
std::this_thread::sleep_for(sleepDuration);
}
}
int main()
{
thread t1(send_thr);
thread t2(recv_thr);
t1.join();
t2.join();
return 0;
}
这个同步队列在没有满的情况下可以插入数据,如果满了,则会调用m_notFull阻塞等待,待消费线程取出数据之后发一个未满的通知,然后前面阻塞的线程就会被唤醒继续往下执行;如果队列为空,就不能取数据,会调用m_notempty条件变量阻塞,等待插入数据的线程发出不为空的通知时,才能继续往下执行。
上述代码用到了std::lock_guard,它利用了RAII机制可以保证安全释放mutex。从上面代码还可以看到,std::unique_lock和std::lock_guard的差别在于前者可以自由的释放mutex,而后者则需要等待std::lock_guard变量声明周期结束时才释放。条件变量的wait还有一个重载方法,可以接受一个条件。比如下面的代码:
std::lock_guard locker(m_mutex);
while(IsFull())
{
m_notFull.wait(m_mutex);
}
可以改为这样:
std::lock_guard locker(m_mutex);
m_notFull.wait(locker, [this]{ return !IsFull();});
两种写法效果是一样的,但是后者更简洁,条件变量先检查判断式是否满足条件,如果满足条件,则重新获取mutex,然后结束wait,继续往下执行;如果不满足条件,则释放mutex,然后将线程置为waiting状态,继续等待。
这里需要注意的是,wait函数中会释放mutex,而lock_guard这时还拥有mutex,它只会在出了作用域之后才会释放mutex,所以,这时它并不会释放,但执行wait时会提前释放mutex。从语义上看这里使用lock_guard会产生矛盾,但是实际上并不会出问题,因为wait提前释放锁之后处于等待状态,在被notify_one或者notify_all唤醒之后会先获取mutex,这相当于lock_guard的mutex在释放之后又获取到了,因此,在出了作用域之后lock_guard自动释放mutex不会出问题。这里应该有unique_lock,因为unique_lock不像lock_guard一样只能在析构时才释放锁,它随时释放锁,因此,在wait时让unique_lock释放锁从语义上看更加准确。
我们可以修改下上面的代码,把std::lock_guard改成std::unique_lock,把sd::condition_variable_any改为std::condition_variable,并且用等待一个判断式的方法来实现一个简单的线程池。
#include
#include
#include
#include
#include
#include
using namespace std;
template
class SyncQueue
{
bool IsFull() const
{
return m_queue.size() == m_maxSize;
}
bool IsEmpty() const
{
return m_queue.empty();
}
public:
SyncQueue(int maxSize) : m_maxSize(maxSize){}
void Put(const T& x)
{
std::lock_guard locker(m_mutex);
m_queue.push_back(x);
m_notEmpty.notify_one();
}
void Take(T& x)
{
std::unique_lock locker(m_mutex);
m_notEmpty.wait(locker, [this]{return !m_queue.empty(); });
x = m_queue.front();
m_queue.pop_front();
}
int Count()
{
return m_queue.size();
}
bool Empty()
{
std::lock_guard locker(m_mutex);
return m_queue.empty();
}
bool Full()
{
std::lock_guard locker(m_mutex);
return m_queue.size() == m_maxSize;
}
private:
std::list m_queue; ///缓冲区
std::mutex m_mutex; ///互斥量和条件变量结合起来使用
std::condition_variable m_notEmpty; ///不为空的条件变量
int m_maxSize; ///同步队列最大的size
};
SyncQueue myqueue(1024);
void send_thr()
{
int i = 0;
char strbuf[100] = {0};
std::chrono::milliseconds sleepDuration(2000);
while(1)
{
snprintf(strbuf, sizeof(strbuf), "value %d", ++i);
cout << "send " << strbuf << endl;
myqueue.Put(strbuf);
std::this_thread::sleep_for(sleepDuration);
if (i > 1000)
{
i = 0;
}
}
}
void recv_thr()
{
std::string str;
std::chrono::milliseconds sleepDuration(1000);
while(1)
{
myqueue.Take(str);
cout << "recv " << str << endl;
std::this_thread::sleep_for(sleepDuration);
}
}
int main()
{
thread t1(send_thr);
thread t2(recv_thr);
t1.join();
t2.join();
return 0;
}
相比于上面的例子,这次使用unique_lock代替lock_guard,使语义更加准确,用性能更好的condition_variable替代condition_variable_any,对程序加以优化,这里仍然用condition_variable_any也是可以的。执行wait时不再通过while循环来判断,而是通过lambda表达式来判断,写法上更简洁了。