• c++多线程并发学习笔记(2)


    等待一个时间或其他条件

    在一个线程等待完成任务时,会有很多选择:

    1. 它可以持续的检查共享数据标志(用于做保护工作的互斥量),直到另一个线程完成工作时对这个标志进行重设。缺点:资源浪费,开销大

    2. 在等待线程的检查间隙,使用std::this_thread::sleep_for()进行周期性的间歇。 缺点:休眠时间抉择困难

    bool flag;
    std::mutex m;
    
    void wait_for_flag()
    {
      std::unique_lock<std::mutex> lk(m);
      while(!flag)
      {
        lk.unlock();  // 1 解锁互斥量
        std::this_thread::sleep_for(std::chrono::milliseconds(100));  // 2 休眠100ms
        lk.lock();   // 3 再锁互斥量
      }
    }

    3. 使用C++标准库提供的工具去等待事件的发生。通过另一线程触发等待事件的机制是最基本的唤醒方式,这种机制就称为“条件变量”。

    C++标准库对条件变量有两套实现:std::condition_variablestd::condition_variable_any这两个实现都包含在<condition_variable>头文件的声明中。两者都需要与一个互斥量一起才能工作(互斥量是为了同步)

    std::condition_variable:只能与std::mutex一起工作,开销少

    std::condition_variable_any:可以和任何满足最低标准的互斥量一起工作,开销大

    std::condition_variable 提供两个重要的接口:notify_one()wait()。wait()可以让线程陷入休眠状态,notify_one()就是唤醒处于wait中的其中一个条件变量(可能当时有很多条件变量都处于wait状态)。

    template<typename Predicate>
    wait(std::unique_lock<std::mutex>& lk, Predicate pred)

    wait()会去检查这些条件(通过调用所提供的函数),当条件满足(调用所提供的函数返回true)时返回。如果条件不满足(调用所提供的函数返回false),wait()函数将解锁互斥量,并且将这个线程置于阻塞或等待状态。另外一个线程调用notify_one()通知条件变量时,线程从睡眠状态中苏醒,重新获取互斥锁,并且再次检查条件是否满足。

    std::condition_variable::wait的一个最小化实现:

    template<typename Predicate>
    void minimal_wait(std::unique_lock<std::mutex>& lk,Predicate pred){
      while(!pred()){
        lk.unlock();
        lk.lock();
      }
    }

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

    #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;
    }

    问题在于,如果生产者的速度比较慢,代码中每隔1s才会有一次数据生产,这时消费者都要去获取锁-->判断队列里是否有数据-->释放锁,这个过程就是资源的浪费,无用功使得cpu占用率很高。

    使用std::this_thread::sleep_for()来对代码进行改造:

    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占用率,但问题在于在实际操作中如何选择休眠时间,太长或者太短都不好。

    最后可以使用条件变量来对这个代码进行改造:

    #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);
            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;
        }
    }
    int main() {
        std::thread t1(function_1);
        std::thread t2(function_2);
        t1.join();
        t2.join();
        return 0;
    }

    需要注意的几点:

    在配合条件变量使用锁时,使用std::unique_lock比std::lock_guard合适,因为在wait内部有对锁的unlock和lock操作

    使用细粒度锁,尽量减小锁的范围,在notify_one()的时候,不需要处于互斥锁的保护范围内,所以在唤醒条件变量之前可以将锁unlock()

    参考资料:

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

    https://chenxiaowei.gitbook.io/c-concurrency-in-action-second-edition-2019/4.0-chinese/4.1-chinese

  • 相关阅读:
    用感知机(Perceptron)实现逻辑AND功能的Python3代码
    xpadder教程:自定义设置游戏手柄的图片
    用Python实现小说中的汉字频率统计
    天猫精灵X1智能音箱使用感想
    一道常被人轻视的前端JS面试题
    jQueryNotes仿QQ空间添加标记
    JQ对象到底是什么
    正则匹配规则
    自定义右键菜单
    IIS处理并发请求时出现的问题及解决
  • 原文地址:https://www.cnblogs.com/duan-shui-liu/p/11447580.html
Copyright © 2020-2023  润新知