当前位置: 首页 > 科技观察

使用条件变量的陷阱你知道吗?

时间:2023-03-16 11:33:33 科技观察

我想大家在开发过程中都会用到多线程。使用多线程时,您基本上会使用条件变量。您了解条件变量只是简单的等待和通知吗?在最近的工作中,同事们似乎都是简单的使用wait和notify,导致项目出现bug,但不知道如何修复。其实还是有一些坑的。程序就总结到这里给大家。1.什么是条件变量?条件变量是多线程程序中实现等待和唤醒逻辑的常用方法。通常有等待和通知两个动作。wait用于阻塞和挂起线程A,直到另一个线程B通过notify唤醒线程A,线程A唤醒后继续运行。条件变量在多线程中非常常用。在著名的生产者消费者问题中,消费者如何通过while循环知道生产者是否生产了可以消费的产品,是否有可以消费的产品?众所周知,死循环对CPU的性能消耗极大,所以需要使用条件变量来阻塞线程,降低CPU占用率。2.条件变量的使用以生产者和消费者问题为例,看下面代码:std::mutexmutex;std::condition_variablecv;std::vectorvec;voidConsume(){std::unique_locklock(mutex);cv.wait(lock);std::cout<<"consume"<lock(mutex);vec.push_back(1);cv.notify_all();std::cout<<"produce\n";}intmain(){std::threadt(Consume);t.detach();Produce();return0;}原意是让消费者线程阻塞,等待生产者生产数据,然后通知消费者线程,让消费者线程拿到数据消费。但是这里有个问题:如果先执行Produce(),再执行Consume(),生产者提前生产数据通知消费者,但是如果此时消费者线程还没有执行wait语句,也就是说,线程如果线程没有在等待条件变量,通知信号就会丢失,等待会在后面的Consume()中执行,但是生产者此时不会再触发notify,所以消费者或者线程会一直阻塞,就会出现bug。如何解决这个问题呢?可以加一个判断条件来解决这个信号丢失问题,看代码:std::mutexmutex;std::condition_variablecv;std::vectorvec;voidConsumer(){std::unique_locklock(mutex);if(vec.empty()){//加上这个判断条件cv.wait(lock);}std::cout<<"consumer"<lock(mutex);vec.push_back(1);cv.notify_all();std::cout<<"produce\n";}intmain(){std::threadt(Consumer);t.detach();Produce();return0;}信号丢失的问题可以通过增加额外的条件来解决,但是还是有一个需要注意的地方,消费的时候线程处于wait阻塞状态,即使不调用notify,操作系统也会有一定的概率唤醒被阻塞的线程,使其继续执行。这就是假唤醒的问题。当发生假唤醒时,消费者线程继续执行,仍然没有可以消费的数据,出现了bug。那么如何解决误唤醒的问题呢?可以在线程从阻塞状态唤醒后继续判断附加条件,看是否满足唤醒条件。如果满意,则继续执行。如果没有,则继续等待,这在代码中有所体现。将if判断改为while循环判断,见代码:std::mutexmutex;std::condition_variablecv;std::vectorvec;voidConsumer(){std::unique_locklock(mutex);while(vec.empty()){//将if改为whilecv.wait(lock);}std::cout<<"consumer"<lock(mutex);vec.push_back(1);cv.notify_all();std::cout<<"produce\n";}intmain(){std::threadt(Consumer);t.detach();Produce();return0;}相信你已经了解了条件变量的使用。需要使用while循环加入判断条件来解决条件变量的信号丢失和误唤醒问题。3、有没有更简单的“避坑”方法?我们每次都必须使用while循环和附加条件来操作条件变量吗?这不是很麻烦吗?不!C++中其实有更好的封装,只是当你需要调用wait函数时,直接在参数中加上额外的条件即可。内部已经做了while循环判断,直接使用即可。见代码:std::mutexmutex;std::condition_variablecv;std::vectorvec;voidConsumer(){std::unique_locklock(mutex);cv.wait(lock,[&](){return!vec.empty();});//这里可以直接使用C++包std::cout<<"consumer"<lock(mutex);vec.push_back(1);cv.notify_all();std::cout<<"produce\n";}intmain(){std::threadt(Consumer);t.detach();Produce();return0;}但是在C语言中就没有办法了,大家只能自己做一层封装。4.为什么条件变量需要和锁一起使用?为什么它们被称为条件变量?因为在内部,线程的阻塞和唤醒是通过判断和修改某个全局变量来决定的。多线程对同一个变量的操作必须加锁才能保证线程安全。同时,一个简单的wait函数调用,内部会非常复杂。有可能线程A调用了wait函数但还没有进入wait阻塞等待,此时另一个线程B调用了notify函数。这时候,nofity的信号就丢失了。如果加了锁,线程B必须等待线程A释放锁进入等待状态,才能调用notify,从而防止信号丢失。关于条件变量的介绍到此结束。希望大家有所收获,在平时的使用中也能避免条件变量的坑。