std::condition_variable.wait()的用法和设计缺陷带来的坑

std::condition_variable的用法

具体用法参照这篇文章,这里只针对其中的一个成员函数 wait() 的用法进行讨论。

成员函数wait()的用法

关于wait()的用法,在这篇文章里说到:

(2)、wait:当前线程调用wait()后将被阻塞,直到另外某个线程调用notify_*唤醒当前线程;当线程被阻塞时,该函数会自动调用std::mutex的unlock()释放锁,使得其它被阻塞在锁竞争上的线程得以继续执行。一旦当前线程获得通知(notify,通常是另外某个线程调用notify_*唤醒了当前线程),wait()函数也是自动调用std::mutex的lock()。wait分为无条件被阻塞和带条件的被阻塞两种。

  • 无条件被阻塞:调用该函数前,当前线程应该已经对unique_lock lck完成了加锁。所有使用同一个条件变量的线程必须在wait函数中使用同一个unique_lock。该wait函数内部会自动调用lck.unlock()对互斥锁解锁,使得其他被阻塞在互斥锁上的线程恢复执行。使用本函数被阻塞的当前线程在获得通知(notified,通过别的线程调用notify_*系列的函数)而被唤醒后,wait()函数恢复执行并自动调用lck.lock()对互斥锁加锁。
  • 带条件的被阻塞:wait函数设置了谓词(Predicate),只有当pred条件为false时调用该wait函数才会阻塞当前线程,并且在收到其它线程的通知后只有当pred为true时才会被解除阻塞。因此,等效于:
    while(!pred()) wait(lck).

其中,带条件的被阻塞是存在坑的,下面将说明。

无条件被阻塞wait()的用法

先写个例子看看 无条件被阻塞wait 的用法:

#include 
#include 
#include 
#include 
#include 
#include 

std::queue<int> queue;
std::mutex queueMutex;
std::condition_variable queueCondVar;

void provider (int val)
{
    std::cout << "provider\n";
    // push different values (val til val+5 with timeouts of val milliseconds into the queue
    for (int i=0; i<6; ++i) {
        {
            std::lock_guard<std::mutex> lg(queueMutex);
            queue.push(val+i);
            std::cout << "val: " << val + i << std::endl;
        } // release locks
        // 去掉condition_variable 的通知,等待该通知的线程应当会一直阻塞
        // queueCondVar.notify_one(); 
		
		// 这一行可要可不要,对下面的输出结论无影响。
        std::this_thread::sleep_for(std::chrono::milliseconds(val));
    }
}

void consumer (int num)
{
    
    // pop values if available (num identifies the consumer)
    while (true) {
        int val;
        {
            std::cout << "consumer " << num << std::endl;
            std::unique_lock<std::mutex> ul(queueMutex);
            // queueCondVar.wait(ul,[]{ return !queue.empty(); });
            queueCondVar.wait(ul);
            val = queue.front();
            queue.pop();
        } // release lock
        std::cout << "consumer " << num << ": " << val << std::endl;
    }
}

int main()
{
    // start three providers for values 100+, 300+, and 500+
    auto p1 = std::async(std::launch::async,provider,100);
    auto p2 = std::async(std::launch::async,provider,300);
    auto p3 = std::async(std::launch::async,provider,500);

    // start two consumers printing the values
    auto c1 = std::async(std::launch::async,consumer,1);
    auto c2 = std::async(std::launch::async,consumer,2);
}

输出结果(注意由于是多个线程在后台运行,每次的运行输出顺序是随机的):
std::condition_variable.wait()的用法和设计缺陷带来的坑_第1张图片
从输出可以看出以下三点:

  1. consumer和provider的五个线程是顺序是随机的,可能三个provider先启动的,正常情况会先执行,但这貌似是不能百分保证??
  2. 与此同时,三个provider都执行完毕,而因为没有收到notify,两个consumer都阻塞在wait()函数处。
  3. 当两个consumer都阻塞在wait()函数处,但是我们发现在输出的顺序里,两行输出:consumer 1 和consumer 2是在中间时期输出的,证明consumer线程的wait()是调用过ul的lock,但是我们发现,provider是继续执行了后续的输出(后面的所有val:…)。因此可以知道,该函数wait()会自动调用std::mutex的unlock()释放锁,使得其它被阻塞在锁竞争上的线程得以继续执行。

带坑的带条件的被阻塞wai()的用法

我们看下这个函数的具体描述:

带条件的被阻塞:wait函数设置了谓词(Predicate),只有当pred条件为false时调用该wait函数才会阻塞当前线程,并且在收到其它线程的通知后只有当pred为true时才会被解除阻塞。因此,等效于:
while(!pred()) wait(lck).

这个谓词也叫做判断式。
引用《c++标准库(第二版)》的内容解释下,这个用法的好处在于处理condition_variable的假醒
std::condition_variable.wait()的用法和设计缺陷带来的坑_第2张图片
也就是说,condition_variable这家伙有时还会逆天了,违抗命令擅自行动,失控了。不过,从上面的例子我们是暂时没有发现这个现象。
我们必须理解,带谓词wait()的实现形式:
在这里插入图片描述
是等于如下形式的:

std::condition_variable.wait()的用法和设计缺陷带来的坑_第3张图片

好了,用法说完了,以while的等效模式来说明下带条件的wait()的存在什么问题:
看下面例子:

#include 
#include 
#include 
#include 
#include 
#include 

std::queue<int> queue;
std::mutex queueMutex;
std::condition_variable queueCondVar;

void provider (int val)
{
    std::cout << "provider\n";
    // push different values (val til val+5 with timeouts of val milliseconds into the queue
    for (int i=0; i<6; ++i) {
        {
            std::lock_guard<std::mutex> lg(queueMutex);
            queue.push(val+i);
            std::cout << "val: " << val + i << std::endl;
        } // release locks
        // queueCondVar.notify_one();
        
		// sleep这行先注释掉,后面对比没有注释掉的情况下有何不同
        // std::this_thread::sleep_for(std::chrono::milliseconds(val));
    }
}

void consumer (int num)
{
    
    // pop values if available (num identifies the consumer)
    while (true) {
        int val;
        {
            std::cout << "consumer " << num << std::endl;
            std::unique_lock<std::mutex> ul(queueMutex);
            queueCondVar.wait(ul,[]{ return !queue.empty(); });
            // queueCondVar.wait(ul);
            val = queue.front();
            queue.pop();
        } // release lock
        std::cout << "consumer " << num << ": " << val << std::endl;
    }
}

int main()
{
    auto p1 = std::async(std::launch::async,provider,100);

    auto c1 = std::async(std::launch::async,consumer,1);
}

两次的输出结果:
std::condition_variable.wait()的用法和设计缺陷带来的坑_第4张图片
我们惊奇的发现,两次结果不一致,而且还有很致命的错误发生了,condition_variable在没有发出任何通知的情况下,wai()执行通过了,还一股脑把所有数据输出了。
我们分析下:

第一次输出情况,consumer 1 线程先执行,线程provider再执行。此时,consumer 1 线程先执行到:

			std::unique_lock<std::mutex> ul(queueMutex);
            queueCondVar.wait(ul,[]{ return !queue.empty(); });

[]{ return !queue.empty(); }的返回值是false,根据while的等效形式,while判断条件通过,进入循环,执行queueCondVar.wait(ul);, 因为在我们的整个程序里,并没有发出任何通知,所以wait()在没有受到queueCondVar的通知以前一直阻塞着。结果也与预期的一致。

第二种输出情况相反:provider 线程先执行,线程consumer 1 再执行。也就是队列queue先有了数据,因此[]{ return !queue.empty(); }的返回值是true,while判断条件不通过,继续执行后面的语句:

val = queue.front();
queue.pop();

因此,知道把queue的所有元素都pop完之前,[]{ return !queue.empty(); }的返回值永远是true,导致queueCondVar.wait(ul);永远不会执行,直到[]{ return !queue.empty(); }返回false,才会进入while循环,执行queueCondVar.wait(ul);等待被通知,所以一直阻塞。

我们在provider的程序里加上sleep来进一步验证上面的结论:

#include 
#include 
#include 
#include 
#include 

std::queue<int> queue;
std::mutex queueMutex;
std::condition_variable queueCondVar;

void provider (int val)
{
    std::cout << "provider\n";
    // push different values (val til val+5 with timeouts of val milliseconds into the queue
    for (int i=0; i<6; ++i) {
        {
            std::lock_guard<std::mutex> lg(queueMutex);
            queue.push(val+i);
            std::cout << "val: " << val + i << std::endl;
        } // release locks
        // queueCondVar.notify_one();

		// 注意,这一行取消注释了,意味着,线程执行到这里会等待,同时queueMutex被解锁,		
		// std::unique_lock ul(queueMutex);得以拿到queueMutex的所有权
        std::this_thread::sleep_for(std::chrono::milliseconds(val));
    }
}

void consumer (int num)
{
    
    // pop values if available (num identifies the consumer)
    while (true) {
        int val;
        {
            std::cout << "consumer " << num << std::endl;
            std::unique_lock<std::mutex> ul(queueMutex);
            queueCondVar.wait(ul,[]{ return !queue.empty(); });
            // queueCondVar.wait(ul);
            val = queue.front();
            queue.pop();
        } // release lock
        std::cout << "consumer " << num << ": " << val << std::endl;
    }
}

int main()
{
    // start three providers for values 100+, 300+, and 500+
    auto p1 = std::async(std::launch::async,provider,100);
    // auto p2 = std::async(std::launch::async,provider,300);
    // auto p3 = std::async(std::launch::async,provider,500);

    // start two consumers printing the values
    auto c1 = std::async(std::launch::async,consumer,1);
    // auto c2 = std::async(std::launch::async,consumer,2);
}

输出结果:
std::condition_variable.wait()的用法和设计缺陷带来的坑_第5张图片
可以发现,comsumer 1 只输出了一次,解释如下:

在provider线程执行一个push后处于睡眠了一段时间,此时comsumer 1 线程第一次时, []{ return !queue.empty(); }的返回值是True,根据while的等效形式,while判断条件不通过,不进入循环。执行下面两条语句,comsumer 1 输出了val的值。

val = queue.front();
queue.pop();

然后,comsumer 1进行下次一周期的运算,由于在provider线程还处于睡眠,queue是空的,执行 []{ return !queue.empty(); }的返回值是false,根据while的等效形式,while判断条件通过,进入循环执行queueCondVar.wait(ul);并阻塞等待通知。因此,后面即使queue存在元素了,在没有受到通知的情况下,也会一直阻塞在当前语句。
因此,comsumer 1 只输出了一次

这估计是这个函数的设计上的一个BUG,只能从程序设计上去规避了。,一种方式就是要保证等待通知的线程先执行。

你可能感兴趣的:(C++,线程,c++11)