c++ - 使用条件变量condition_variable, 什么条件下会虚假唤醒?

查看:1708
本文介绍了c++ - 使用条件变量condition_variable, 什么条件下会虚假唤醒?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

问 题

直接拿wiki上的例子描述吧.
https://en.wikipedia.org/wiki...

/* In any waiting thread: */
while(!buf->full)
    wait(&buf->cond, &buf->lock);

/* In any other thread: */
// 这种写法,如果拿到了条件变量后,buf->full状态有变(可能是被其他唤醒的线程搞起来了),此线程按理说就不应该唤醒了.因此导致虚假唤醒
if(buf->n >= buf->size){
    buf->full = 1;
    signal(&buf->cond);
}

映射到c++11的例子如下,条件变量原语用condition_variable代替.
惯用法如下:

std::condition_variable con_var;
...
while(!notified)
    con_var.wait(lock);   // ok,没有问题,不存在虚假唤醒
....
if(!notified) con_var.wait(lock)  // Nok, con_var状态更改后,此线程被唤醒,如果此时notified被另外一个线程又修改成false了,那此线程不需要被唤醒干活->虚假唤醒

我的疑问是:

参照condition_variabl描述,并没有给出虚假唤醒的触发条件. 而condition_variable有两个成员变量notify_onenotify_all
notify_one: 只会唤醒一个等待中的线程
notify_all: 唤醒所有等待中的线程
那么,是否只有用了notify_all这个接口才会触发虚假唤醒,如果代码中使用了第一个,完全不需要关注这个? 除此之外,还有什么条件会触发虚假唤醒么?

补充

我就是这里有疑问,最开始也理解虚假唤醒应该是notify_one触发.

但实际上我按照会虚假唤醒写法去触发,并没有触发出来,这里是代码,编译g++ -std=c++14 -pthread
例子是cppref上改的,一个线程producer作为生产者,consumer/consumer2线程是两个消费者线程,如果队列里面有数据产生,producer发送notify_one出来,唤醒消费者线程.
预期:两个等待状态的线程每次都被唤醒,而第一个个被唤醒的线程把数据取出来后,活就干完了,不需要另外个线程了.
实际:两个线程总是交替被唤醒.

// condition_variable.cpp

#include <condition_variable>
#include <mutex>
#include <thread>
#include <iostream>
#include <queue>
#include <chrono>
using namespace std;
using namespace std::chrono_literals;
int main()
{
    // lock
    std::queue<int> produced_nums;
    std::mutex m;
    std::condition_variable con_var;

    // variable
    bool done = false;
    bool notified = false;

    std::thread producer([&](){
            for(int i=0; i<5; ++i) {
                std::this_thread::sleep_for(std::chrono::seconds(2));
                std::unique_lock<std::mutex> lock(m);
                std::cout<<"producing "<<i<<endl;
                produced_nums.push(i);

                notified = true;
                // con_var.notify_all();
                con_var.notify_one();
                std::this_thread::sleep_for(std::chrono::seconds(2));
            }

            done = true;
            con_var.notify_all();
        });

    std::thread consumer([&]() {
            std::unique_lock<std::mutex> lock(m);
            while(!done) {
                //con_var.wait(lock, [&]{return notified;});
                if(!notified) con_var.wait(lock);
                while(!produced_nums.empty()) {
                    std::cout<<"consuming " <<produced_nums.front()<<" consumer1"<<endl;
                    produced_nums.pop();
                }
                std::cout<<"thread id: 1 "<<"wake up"<<std::endl; 
                notified = false;
            }
            notified = true;
    });

    std::thread consumer2([&]() {
            std::unique_lock<std::mutex> lock(m);
            while(!done) {
                std::this_thread::sleep_for(std::chrono::seconds(1));
                //con_var.wait(lock, [&]{return notified;});
                if(!notified) con_var.wait(lock);
                while(!produced_nums.empty()) {
                    std::cout<<"consuming " <<produced_nums.front()<<" consumer2"<<endl;
                    produced_nums.pop();
                }
                std::cout<<"thread id: 2 "<<"wake up"<<std::endl; 
                notified = false;
            }
            notified = true;
    });

    producer.join();
    consumer.join();
    consumer2.join();
    /*
    std::this_thread::sleep_for(1s);
    std::cout<<"id1:"<<consumer.get_id()<<endl;
    std::cout<<"id2:"<<consumer2.get_id()<<std::endl;
    std::cout<<"producer:"<<producer.get_id()<<std::endl;
    */
}

这里运行结果:
// consumer1 & consumer2 被交替唤醒.

producing 0
consuming 0 consumer1
thread id: 1 wake up
producing 1
consuming 1 consumer2
thread id: 2 wake up
producing 2
consuming 2 consumer1
thread id: 1 wake up
producing 3
consuming 3 consumer2
thread id: 2 wake up
producing 4
consuming 4 consumer1
thread id: 1 wake up
thread id: 2 wake up

解决方案

尝试回答一下刷新下当前对condition_variable的理解.

首先, 虚假唤醒这个东西不属于c++, 而是基于互斥锁条件变量这两个原语构建的线程同步机制可能产生的一个问题. stackoverflow上也有相关的讨论,看到的东西也只是wiki上的描述而已.

具体到c++11的condition_variable来说,最佳实践如下,

con.wait(lock, [](){return status});

可以理解为标准三件套:条件变量/互斥锁/线程共享状态量. 只有这三个条件都满足的时候,线程才会被唤醒. 条件变量原语的时序可以参考下图:

那么是不是可以找到理论上触发虚假唤醒的条件呢? 简化了测试代码,确实是触发了,消息发送使用notify_one, 生产者-消费者都只有一个(因为notify_one只会试图唤醒一个waiting中的线程,多了消费者,也没用), 我们看看官方给出的可能会发生虚假唤醒的调用语句:

if(!status) con.wait(lock);

实际上就是把互斥锁和共享状态量分开,那就好办了,构造时序:消费者线程等待的时候,notify_one唤醒线程,并释放互斥锁,延迟修改共享状态量(休眠2s, 这2s内消费者线程已经可以拿到锁和条件变量了),那这线程就本不该被唤醒,发生了虚假唤醒.生产者部分代码如下:

con_var.notify_one();
// trigger the spurious wakeup
lock.unlock();
std::this_thread::sleep_for(std::chrono::seconds(2));
notified = true;

总结

虽然触发了虚假唤醒, 但是没有感觉有什么意义, 一则作为消息生产者来说,在互斥锁的控制下,消息发送和状态量变更本来就应该一起操作,脱离了锁修改变量,那本身也是写的有问题; 对于消费者线程来说,互斥量/条件变量/共享状态变量,写在一个等效"原子操作"里面是一件很自然而然的事情,连while循环的写法都不利于表达意图,更加没有出错的可能了. 按照最佳实践来写,没错儿.

这篇关于c++ - 使用条件变量condition_variable, 什么条件下会虚假唤醒?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆