• [c++11]多线程编程(六)——条件变量(Condition Variable)


    https://www.jianshu.com/p/c1dfa1d40f53

    [c++11]多线程编程(六)——条件变量(Condition Variable)

        <!-- 作者区域 -->
        <div class="author">
          <a class="avatar" href="/u/a549acfa2f33">
            <img src="//upload.jianshu.io/users/upload_avatars/4427263/51aff4c0-f91b-4884-bdd0-f1e9bf4c13f4.jpg?imageMogr2/auto-orient/strip|imageView2/1/w/96/h/96" alt="96">
    


    StormZhu






    0.5


    2018.09.02 01:45*
    字数 1339
    阅读 8391评论 3



        <!-- 文章内容 -->
        <div data-note-content="" class="show-content">
          <div class="show-content-free">
            <p>互斥锁<code>std::mutex</code>是一种最常见的线程间同步的手段,但是在有些情况下不太高效。</p>
    

    假设想实现一个简单的消费者生产者模型,一个线程往队列中放入数据,一个线程往队列中取数据,取数据前需要判断一下队列中确实有数据,由于这个队列是线程间共享的,所以,需要使用互斥锁进行保护,一个线程在往队列添加数据的时候,另一个线程不能取,反之亦然。用互斥锁实现如下:

    #include <iostream>
    #include <deque>
    #include <thread>
    #include <mutex>
    

    std::deque<int> q;
    std::mutex mu;

    void function_1() {
    int count = 10;
    while (count > 0) {
    std::unique_lock<std::mutex> locker(mu);
    q.push_front(count);
    locker.unlock();
    std::this_thread::sleep_for(std::chrono::seconds(1));
    count--;
    }
    }

    void function_2() {
    int data = 0;
    while ( data != 1) {
    std::unique_lock<std::mutex> locker(mu);
    if (!q.empty()) {
    data = q.back();
    q.pop_back();
    locker.unlock();
    std::cout << "t2 got a value from t1: " << data << std::endl;
    } else {
    locker.unlock();
    }
    }
    }
    int main() {
    std::thread t1(function_1);
    std::thread t2(function_2);
    t1.join();
    t2.join();
    return 0;
    }

    //输出结果
    //t2 got a value from t1: 10
    //t2 got a value from t1: 9
    //t2 got a value from t1: 8
    //t2 got a value from t1: 7
    //t2 got a value from t1: 6
    //t2 got a value from t1: 5
    //t2 got a value from t1: 4
    //t2 got a value from t1: 3
    //t2 got a value from t1: 2
    //t2 got a value from t1: 1

    可以看到,互斥锁其实可以完成这个任务,但是却存在着性能问题。

    首先,function_1函数是生产者,在生产过程中,std::this_thread::sleep_for(std::chrono::seconds(1));表示延时1s,所以这个生产的过程是很慢的;function_2函数是消费者,存在着一个while循环,只有在接收到表示结束的数据的时候,才会停止,每次循环内部,都是先加锁,判断队列不空,然后就取出一个数,最后解锁。所以说,在1s内,做了很多无用功!这样的话,CPU占用率会很高,可能达到100%(单核)。如图:

    CPU占用率.png

    解决办法之一是给消费者也加一个小延时,如果一次判断后,发现队列是空的,就惩罚一下自己,延时500ms,这样可以减小CPU的占用率。

    void function_2() {
        int data = 0;
        while ( data != 1) {
            std::unique_lock<std::mutex> locker(mu);
            if (!q.empty()) {
                data = q.back();
                q.pop_back();
                locker.unlock();
                std::cout << "t2 got a value from t1: " << data << std::endl;
            } else {
                locker.unlock();
                std::this_thread::sleep_for(std::chrono::milliseconds(500));
            }
        }
    }
    

    如图:

    使用延时的CPU占用率.png

    然后困难之处在于,如何确定这个延时时间呢,假如生产者生产的很快,消费者却延时500ms,也不是很好,如果生产者生产的更慢,那么消费者延时500ms,还是不必要的占用了CPU。

    这就引出了条件变量(condition variable),c++11中提供了#include <condition_variable>头文件,其中的std::condition_variable可以和std::mutex结合一起使用,其中有两个重要的接口,notify_one()wait()wait()可以让线程陷入休眠状态,在消费者生产者模型中,如果生产者发现队列中没有东西,就可以让自己休眠,但是不能一直不干活啊,notify_one()就是唤醒处于wait中的其中一个条件变量(可能当时有很多条件变量都处于wait状态)。那什么时刻使用notify_one()比较好呢,当然是在生产者往队列中放数据的时候了,队列中有数据,就可以赶紧叫醒等待中的线程起来干活了。

    使用条件变量修改后如下:

    #include <iostream>
    #include <deque>
    #include <thread>
    #include <mutex>
    #include <condition_variable>
    

    std::deque<int> q;
    std::mutex mu;
    std::condition_variable cond;

    void function_1() {
    int count = 10;
    while (count > 0) {
    std::unique_lock<std::mutex> locker(mu);
    q.push_front(count);
    locker.unlock();
    cond.notify_one(); // Notify one waiting thread, if there is one.
    std::this_thread::sleep_for(std::chrono::seconds(1));
    count--;
    }
    }

    void function_2() {
    int data = 0;
    while ( data != 1) {
    std::unique_lock<std::mutex> locker(mu);
    while(q.empty())
    cond.wait(locker); // Unlock mu and wait to be notified
    data = q.back();
    q.pop_back();
    locker.unlock();
    std::cout << "t2 got a value from t1: " << data << std::endl;
    }
    }
    int main() {
    std::thread t1(function_1);
    std::thread t2(function_2);
    t1.join();
    t2.join();
    return 0;
    }

    此时CPU的占用率也很低。

    使用条件变量时的CPU占用率.png

    上面的代码有三个注意事项:

    1. function_2中,在判断队列是否为空的时候,使用的是while(q.empty()),而不是if(q.empty()),这是因为wait()从阻塞到返回,不一定就是由于notify_one()函数造成的,还有可能由于系统的不确定原因唤醒(可能和条件变量的实现机制有关),这个的时机和频率都是不确定的,被称作伪唤醒,如果在错误的时候被唤醒了,执行后面的语句就会错误,所以需要再次判断队列是否为空,如果还是为空,就继续wait()阻塞。
    2. 在管理互斥锁的时候,使用的是std::unique_lock而不是std::lock_guard,而且事实上也不能使用std::lock_guard,这需要先解释下wait()函数所做的事情。可以看到,在wait()函数之前,使用互斥锁保护了,如果wait的时候什么都没做,岂不是一直持有互斥锁?那生产者也会一直卡住,不能够将数据放入队列中了。所以,wait()函数会先调用互斥锁的unlock()函数,然后再将自己睡眠,在被唤醒后,又会继续持有锁,保护后面的队列操作。lock_guard没有lockunlock接口,而unique_lock提供了。这就是必须使用unique_lock的原因。
    3. 使用细粒度锁,尽量减小锁的范围,在notify_one()的时候,不需要处于互斥锁的保护范围内,所以在唤醒条件变量之前可以将锁unlock()

    还可以将cond.wait(locker);换一种写法,wait()的第二个参数可以传入一个函数表示检查条件,这里使用lambda函数最为简单,如果这个函数返回的是truewait()函数不会阻塞会直接返回,如果这个函数返回的是falsewait()函数就会阻塞着等待唤醒,如果被伪唤醒,会继续判断函数返回值。

    void function_2() {
        int data = 0;
        while ( data != 1) {
            std::unique_lock<std::mutex> locker(mu);
            cond.wait(locker, [](){ return !q.empty();} );  // Unlock mu and wait to be notified
            data = q.back();
            q.pop_back();
            locker.unlock();
            std::cout << "t2 got a value from t1: " << data << std::endl;
        }
    }
    

    除了notify_one()函数,c++还提供了notify_all()函数,可以同时唤醒所有处于wait状态的条件变量。

    参考

    1. C++并发编程实战
    2. C++ Threading #6: Condition Variable
          </div>
        </div>
    </div>
    无欲则刚 关心则乱
  • 相关阅读:
    ASP.NET Web API +Swagger创建与汉化生成 API说明文档
    Apple 开发者账号 All In One
    CS50 2022 All In One
    TypeScript private field All In One
    js RegExp test bug All In One
    vite preview not work All In One
    Flutter Resources All In One
    table 组件性能优化 All In One
    Stanford CS193p All In One
    Swift 5.x bug All In One
  • 原文地址:https://www.cnblogs.com/xjyxp/p/11327854.html
Copyright © 2020-2023  润新知