37、条件变量
大家平时编程应该都会用到条件变量,但我估计很多人都不能正确的使用条件变量。
条件变量其实不只是简单的wait
和notify
,这里面还是有些坑的。
什么是条件变量?
条件变量是多线程程序中用来实现等待和唤醒逻辑常用的方法。通常有wait
和notify
两个动作,wait
用于阻塞挂起线程A
,直到另一个线程B
通过通过notify
唤醒线程A
,唤醒后线程A
会继续运行。
条件变量在多线程中很常用,在有名的生产者和消费者问题中,消费者如何知道生成者是否生产出了可以消费的产品,通过while
循环不停的去判断是否有可消费的产品?
死循环极其消耗CPU
性能,所以需要使用条件变量来阻塞线程,降低CPU
占用率。
条件变量的使用
拿生产者和消费者问题举例,看下面这段代码:
std::mutex mutex;
std::condition_variable cv;
std::vector<int> vec;
void Consume() {
std::unique_lock<std::mutex> lock(mutex);
cv.wait(lock);
std::cout << "consume " << vec.size() << "\n";
}
void Produce() {
std::unique_lock<std::mutex> lock(mutex);
vec.push_back(1);
cv.notify_all();
std::cout << "produce \n";
}
int main() {
std::thread t(Consume);
t.detach();
Produce();
return 0;
}
本意是消费者线程阻塞,等待生产者生产数据后去通知消费者线程,这样消费者线程就可以拿到数据去消费。
但这里有个问题:
如果先执行的Produce()
,后执行的Consume()
,生产者提前生产出了数据,去通知消费者,但是此时消费者线程如果还没有执行到wait
语句,即线程还没有处于挂起等待状态,线程没有等待此条件变量上,那通知的信号就丢失了,后面Consume()
中才执行wait
处于等待状态,但此时生产者已经不会再触发notify
,那消费者线程就会始终阻塞下去,出现bug
。
如何解决这个问题呢?可以附加一个判断条件,就可以解决这种信号丢失问题,见代码:
void Consumer() {
std::unique_lock<std::mutex> lock(mutex);
if (vec.empty()) { // 加入此判断条件
cv.wait(lock);
}
std::cout << "consumer " << vec.size() << "\n";
}
void Produce() {
std::unique_lock<std::mutex> lock(mutex);
vec.push_back(1);
cv.notify_all();
std::cout << "produce \n";
}
通过增加附加条件可以解决信号丢失的问题,但这里还有个地方需要注意,消费者线程处于wait
阻塞状态时,即使没有调用notify
,操作系统也会有一些概率会唤醒处于阻塞的线程,使其继续执行下去,这就是虚假唤醒问题,当出现了虚假唤醒后,消费者线程继续执行,还是没有可以消费的数据,出现了bug
。
什么是虚假唤醒?
没必要唤醒的任务被唤醒了就是虚假唤醒。
为什么会有虚假唤醒的问题?
你可以理解为操作系统实现的一个bug
,总之会有这个问题。条件变量为什么会出现虚假唤醒问题
如何解决虚假唤醒问题?
可以在线程由阻塞状态被唤醒后继续判断附加条件,看是否满足唤醒的条件,如果满足则继续执行,如果不满足,则继续去等待,体现在代码中,即将if
判断改为while
循环判断,见代码:
void Consumer() {
std::unique_lock<std::mutex> lock(mutex);
while (vec.empty()) { // 将if改为while
cv.wait(lock);
}
std::cout << "consumer " << vec.size() << "\n";
}
void Produce() {
std::unique_lock<std::mutex> lock(mutex);
vec.push_back(1);
cv.notify_all();
std::cout << "produce \n";
}
看到这里相信你已经明白条件变量的使用啦,需要使用while循环附加判断条件来解决条件变量的信号丢失和虚假唤醒问题。
有没有简单的避坑方式?
难道我们每次都必须要使用while
循环和附加条件来操作条件变量吗?这岂不是很麻烦?
在C++
中其实有更好的封装,只需要调用wait
函数时,在参数中直接添加附加条件就好了,内部已经做好了while
循环判断,直接使用即可,见代码:
void Consumer() {
std::unique_lock<std::mutex> lock(mutex);
cv.wait(lock, [&]() { return !vec.empty(); }); // 这里可以直接使用C++的封装
std::cout << "consumer " << vec.size() << "\n";
}
void Produce() {
std::unique_lock<std::mutex> lock(mutex);
vec.push_back(1);
cv.notify_all();
std::cout << "produce \n";
}
C++
中可以很方便的使用条件变量,如果你使用的是C语言
,那就需要自己封装一下啦。
条件变量为什么需要和锁配合使用?
为什么叫条件变量呢?内部会通过判断及修改某个全局变量来决定线程的阻塞与唤醒,多线程操作同一个变量肯定需要加锁来使得线程安全。同时,一个简单的wait
函数调用内部会很复杂的,有可能线程A
调用了wait
函数但是还没有进入到wait
阻塞等待前,另一个线程B
在此时却调用了notify
函数,此时nofity
的信号就丢失啦,如果加了锁,线程B
必须等待线程A
释放了锁并进入了等待状态后才可以调用notify
,继而防止信号丢失。
