【一分钟学C++】条件变量

在这里插入图片描述

竹杖芒鞋轻胜马,谁怕?一蓑烟雨任平生~
公众号: C++学习与探索  |  个人主页: rainInSunny  |  个人专栏: Learn OpenGL In Qt

基本用法

  条件变量作为生产中者消费者模型中的典型临界区控制方案,在写代码过程中还是经常会使用到的,涉及到多线程总是会有很多坑,这里总结下 C++ 中条件变量 std::condition_variable。cppreference 给出的官方解释:

A condition variable is an object able to block the calling thread until notified to resume. It uses a unique_lock (over a mutex) to lock the thread when one of its wait functions is called. The thread remains blocked until woken up by another thread that calls a notification function on the same condition_variable object.

  大概是说:条件变量是一个能够阻塞调用线程,直到被通知恢复的对象。它使用 unique_lock(基于 mutex)来锁定线程,当调用它的等待函数时,线程会被阻塞。线程会一直处于阻塞状态,直到另一个线程调用同一条件变量对象上的通知函数,将其唤醒。

核心接口

  • wait:等待直到被唤醒,会阻塞当前线程直到被唤醒。
  • wait_for:等待直到超时或者被唤醒。
  • wait_until:等待直到到达某一时刻或者被唤醒。

示例代码

// condition_variable example
#include <iostream>           // std::cout
#include <thread>             // std::thread
#include <mutex>              // std::mutex, std::unique_lock
#include <condition_variable> // std::condition_variable

std::mutex mtx;
std::condition_variable cv;
bool ready = false;

void print_id (int id) {
    std::unique_lock<std::mutex> lck(mtx);
    while (!ready) cv.wait(lck);
    // ...
    std::cout << "thread " << id << '\n';
}

void go() {
    std::unique_lock<std::mutex> lck(mtx);
    ready = true;
    cv.notify_all();
}

int main ()
{
    std::thread threads[10];
    // spawn 10 threads:
    for (int i=0; i<10; ++i)
        threads[i] = std::thread(print_id,i);

    std::cout << "10 threads ready to race...\n";
    go();                       // go!

    for (auto& th : threads) th.join();

    return 0;
}

  基本逻辑比较简单,主线程创建了十个子线程,用于执行 print_id 函数过程,print_id 中根据 ready 变量的状态决定是否等待,go 函数中改变 ready 状态并唤醒等待中的线程。

值得讨论的几个问题

  • notify_one 是不是一定只会唤醒一个阻塞线程?
      顾名思义,很容易认为 notify_one 就是唤醒一个线程,但是查阅资料显示,并不一定保证只唤醒一个阻塞线程。
    在这里插入图片描述
      图中显示 pthread_cond_signal() 函数应当解锁至少一个在指定条件变量 cond 上被阻塞的线程(如果有线程在 cond 上被阻塞的话),也就是并不保证有且仅有一个线程被唤醒。具体可以参考:https://linux.die.net/man/3/pthread_cond_signal

  • 虚假唤醒问题
      虚假唤醒和唤醒丢失问题核心都在 while (!ready) cv.wait(lck); 这行代码上,虚假唤醒存在于这里的 while 替换成 if 的场景,当 wait 阻塞线程后,条件变量满足条件发出唤醒信号,如果不止一个阻塞线程被唤醒,那么可能造成虚假唤醒,如下面代码。

void Producer()
{
    std::this_thread::sleep_for(2000ms);
    std::cout << "Ready Send notification." << std::endl;
    mtx.lock();
    q.push("message");
    mtx.unlock();
    cv.notify_all();
 }

void Consumer()
{
    std::cout << "Wait for notification." << std::endl;
    std::unique_lock<std::mutex> lck(mtx);
    if(q.empty())
        cv.wait(lck);
    std::string msg = q.front();
    q.pop();
    mtx.unlock();
    std::cout << "Get: " << msg <<std::endl;
}

  这里 while 被替换成了 if(q.empty()),当多个线程被同时唤醒,而消息队列里面只有一个消息,那么只有一个线程能处理这个消息,其余线程处理过程中,消息队列都为空,就出现了虚假唤醒,此时应该将 if 替换为 while,就不会导致其他线程错误进入处理状态,而是继续等待。

  • 唤醒丢失问题
      唤醒丢失问题是指没有 while 这一行判断的情况下,如果 wait 线程还没有走到 wait 逻辑,notify 就触发了,后续进行 wait 永远都不会收到 notify 信号,导致卡死。解决方法也比较简单,需要构造条件,当满足条件时才进行 wait 等待。例如上面例子中的 ready 变量就起到这个作用,即使 notify 先触发了,ready 也会被设置为 true,这样等待线程不会执行 wait 逻辑,也就不会卡死。

  • 为什么条件变量一定要和锁绑定?
      第一次加锁就是一个简单的临界区保护,条件变量需要传入一个 std::unique_lock 类型作为参数,是因为在 wait 执行后需要释放锁,让其它线程能进入临界区,比如 notify 线程。在 wait 结束后,需要再次尝试获取锁,如果没有这个过程,可能导致多个线程同时进入临界区,因为有多个线程可能被同时唤醒。

关注公众号:C++学习与探索,有惊喜哦~

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值