条件变量std::condition_variable
什么是条件变量?
线程间的同步有这样一种情况:线程A需要等某个条件成立才能继续往下执行,现在这个条件不成立,线程A就阻塞等待,而线程B在执行过程中使这个条件成立了,于是唤醒线程A继续执行。条件变量就是用在这种场合,它让一个线程等条件变量而挂起,另一个线程在条件满足时再发送信号去通知等待的线程。
最典型的一个问题是生产者与消费者问题:两个线程共享一个公共的固定大小的缓冲区。其中一个是生产者,用于将数据放入缓冲区,持续进行。另一个是消费者,用于从缓冲区中读取数据,持续进行。当缓冲区满时,此时如果生产者必须等待消费者取出一个数据后才能继续存放数据,必须等待。当缓冲区空时,如果消费都还想去取数据,要等生产者存放一个数据才能去取,这段时间也必须等待。
假设用一个全局变量count表示当前缓冲区中的数据个数,N表示缓冲区的总大小。线程A充当消费者,线程B充当生产者。当缓冲区为空时,线程A 读取count = 0,准备休眠(sleep)。而此时线程B打算生产数据,当生产一个数据时,由于 count + 1,由此推断线程A已经休眠,所以线程B会发一个信号给线程A,让其恢复消费,继续运行。由于线程A与B没有对count进行互斥量加锁,所以可能A准备休眠之前,线程B给线程A发了信号(count已经不为0),但是由于线程A读取的还是count = 0 (没有加锁),导致信号被忽略。所以A继续休眠,B一直生产。最终,B也因为缓冲区满,也休眠。线程相互等待,造成死锁。
因此,条件变量必须绑定一个互斥量,当条件不满足时,进行如下原子操作:线程将mutex解锁、线程被条件变量阻塞。这是一个原子操作,不会被打断。当条件变量状态改变,被通知时(条件不一定满足,如虚假唤醒),重新判断当前条件是否满足,对互斥量加锁,进行操作,再解锁。
std::condition_variable、notify_one()、wait()
1 std::condition_variable 是一个类模板,仅限于与 std::mutex 一起工作
2 只有使用std::unique_lock对mutex进行管理,因为在条件变量wait()期间,要对mutex解锁。
#include <iostream>
#include <thread>
#include <list>
#include <mutex>
using namespace std;
class A
{
public:
// 把收到的消息传入队列
void inMsgRecvQueue()
{
for (size_t i = 0; i < 1000; ++i)
{
cout << "收到消息,并放入队列 " << i << endl;
std::unique_lock<std::mutex> my_uniq(my_mutex);
msgRecvQueue.push_back(i);
my_condi.notify_one(); // 我们尝试把my_condi.wait()唤醒,通知它。
}
cout << "消息入队结束" << endl;
}
// 从队列中取出消息
void outMsgRecvQueue()
{
while (true)
{
// 获取my_mutex,并加锁
std::unique_lock<std::mutex> my_uniq(my_mutex);
// wait()等待一个通知和一个条件。如果第二个参数lambda表达示返回值是false,那么wait()将解锁互斥量。并堵塞到本行。
// 堵塞到什么时候为止呢?堵塞到其它某个线程调用notify_one()成员函数为止;
// 如果wait()没有第二个参数:my_condi.wait(my_uniq), 那么就跟第二个参数lambda表达示返回false一样。
// 当其它线程用notify_one()通知本线程时,wait就开始恢复工作,wait()不断尝试,重新获取互斥量的锁。
// 如果获取不到锁,流程就卡在wait()这,继续等待加锁互斥量。
// 如果wait有第二个参数(lambda),就判断lambda表达式,如果lambda表达式返回false,那么wait又对互斥量解锁。继续等待下一次notify
// 如果lambda表达式返回true,则wait()返回,流程往下走。
// 如果wait没有第二个参数,则wait()返回,流程走下来。
my_condi.wait(my_uniq, [this] {
if (!msgRecvQueue.empty())
return true;
else
return false;
});
// 如果流程走到这来,互斥量一定是加锁的。
int command = msgRecvQueue.front();
msgRecvQueue.pop_front();
my_uniq.unlock(); // 提前解锁,避免锁住太长时间
// 其它业务
cout << "出队列:" << command << endl;
}
}
private:
list<int> msgRecvQueue;
mutex my_mutex;
std::condition_variable my_condi; // 生成一个条件对象
};
int main()
{
A myobj;
thread myInMsgObj(&A::inMsgRecvQueue, &myobj);
thread myOutMsgObj(&A::outMsgRecvQueue, &myobj);
myInMsgObj.join();
myOutMsgObj.join();
return 0;
}
上述代码中隐藏的问题:
由于条件变量要一直等待 inMsgRecvQueue()线程中通知,并且,即使收到知道后,还需要去获取my_mutex的加锁,才能继续走outMsgRecvQueue()的流程。这样就会导致,即使两个线程同时运行,会使msgRecvQueue队列中存入了大量了数据,而只取了少量的数据。最终,存数据线程已经走完,而取数据线程并没有取出其中所有的数据。
notify_all()
notify_one()只能通知另外的一个线程去尝试获取锁,在有多个线程等待条件变量的情况下,只能有一个线程能去尝试获取锁,唤醒哪个线程,不能确定。但是,notify_all()时,其它的所有线程都会唤醒,有机会去获取mutex加锁,但是只有一个线程能获取锁成功。