• C++并发编成 03 线程同步


    这一节主要讲讲线程同步的方式,C++ 11中提供了丰富的线程同步元语,如condition_variable,futrue,std::packaged_task<>,std::promise,std::async等,本节后续内容将就这些话题进行阐述。

    1. Lambda表达式

    lambda表达式是C++ 11提供的新特性,在高级语言当中,该语法特性已经得到了普遍的支持。lambda函数能够大大简化代码复杂度(语法糖:利于理解具体的功能),避免实现调用对象。能为等待函数,例如 std::condition_variable提供很好谓词函数,其语义可以用来快速的表示可访问的变量,而非使用类中函数来对成员变量进行捕获。在开始讲线程同步的内容之前,我想简单讲讲lambda表达式,主要是后面在写示例代码的时候会使用到lambda表达式的知识,就算是做个知识的提前铺垫吧。

     lambda表达书以方括号开头,所有操作语义都在一对大括号中{ },并以()结尾,

    
    
    [capture list] (params list) mutable exception-> return type { function body }

    各项具体含义如下

    1. capture list:捕获外部变量列表
    2. params list:形参列表
    3. mutable指示符:用来说用是否可以修改捕获的变量
    4. exception:异常设定
    5. return type:返回类型
    6. function body:函数体

    此外,我们还可以省略其中的某些成分来声明“不完整”的Lambda表达式,常见的有以下几种:

    序号格式
    1 [capture list] (params list) -> return type {function body}
    2 [capture list] (params list) {function body}
    3 [capture list] {function body}

    其中:

    • 格式1声明了const类型的表达式,这种类型的表达式不能修改捕获列表中的值。
    • 格式2省略了返回值类型,但编译器可以根据以下规则推断出Lambda表达式的返回类型: (1):如果function body中存在return语句,则该Lambda表达式的返回类型由return语句的返回类型确定; (2):如果function body中没有return语句,则返回值为void类型。
    • 格式3中省略了参数列表,类似普通函数中的无参函数。

    例 1 使用了lambda表达式对以一个verctor容器中的所有元素进行的打印.

    std::vector<int> data={1,2,3,4,5,6,7,8,9};
    std::for_each(data.begin(),data.end(),[](int i){std::cout<<i<<"
    ";});

    例2 容器比较函数

    #include <iostream>
    #include <vector>
    #include <algorithm>
    using namespace std;
    
    bool cmp(int a, int b)
    {
        return  a < b;
    }
    
    int main()
    {
        vector<int> myvec{ 3, 2, 5, 7, 3, 2 };
        vector<int> lbvec(myvec);
    
        sort(myvec.begin(), myvec.end(), cmp); // 旧式做法
        cout << "predicate function:" << endl;
        for (int it : myvec)
            cout << it << ' ';
        cout << endl;
    
        sort(lbvec.begin(), lbvec.end(), [](int a, int b) -> bool { return a < b; });   // Lambda表达式
        cout << "lambda expression:" << endl;
        for (int it : lbvec)
            cout << it << ' ';
    }

    在C++11之前,我们使用STL的sort函数,需要提供一个谓词函数。如果使用C++11的Lambda表达式,我们只需要传入一个匿名函数即可,方便简洁,而且代码的可读性也比旧式的做法好多了。

     

    Lambda表达式通过在最前面的方括号[]来明确指明其内部可以访问的外部变量,主要有值捕获、引用捕获、隐式捕获三种。

    捕获形式说明
    [] 不捕获任何外部变量
    [变量名, …] 默认以值得形式捕获指定的多个外部变量(用逗号分隔),如果引用捕获,需要显示声明(使用&说明符)
    [this] 以值的形式捕获this指针
    [=] 以值的形式捕获所有外部变量
    [&] 以引用形式捕获所有外部变量
    [=, &x] 变量x以引用形式捕获,其余变量以传值形式捕获
    [&, x] 变量x以值的形式捕获,其余变量以引用形式捕获

    举例:

    #include <iostream>
    using namespace std;
    
    int main()
    {
        int a = 123;
        auto f = [a] { cout << a << endl; };   // 值捕获外部本地变量a
        f(); // 输出:123
    
        //或通过“函数体”后面的‘()’传入参数
        auto x = [](int a){cout << a << endl; return 0; }(123);  // 显示传入外部变量值(值传递)给lambda表达式,输出123
    
    
        auto refFunction = [&a] { cout << a << endl; };  // 通过lambda表达式定义函数,引用捕获外部变量
        a = 321;
        refFunction(); //调用lambda函数,输出:321
    
        
        auto fvalue = [=] { cout << a << endl; };    // 隐式值捕获外部所有变量
        fvalue(); // 输出:321
    
        auto fref = [&] { cout << a << endl; };    // 隐式引用捕获外部所有变量
        a = 321;
        fref(); // 输出:321
    }

     2. std::condition_variable

    std::condition_variable可以被多个thread都可以访问的并等待条件达成,如果条件未达成,则访问线程阻塞;反之,如果条件达成,可以在达成的线程内调用std::condition_variable.notify_one()或std::condition_variable.notify_all来唤醒一个或全部等待线程。等待线程收到通知,就知道条件满足,就可以继续执行了。std::condition_variable需要和std::mutex,std::unique_lock一起配合来使用

     1 #include <mutex>
     2 #include <condition_variable>
     3 #include <thread>
     4 #include <queue>
     5 
     6 bool more_data_to_prepare()
     7 {
     8     return false;
     9 }
    10 
    11 struct data_chunk
    12 {};
    13 
    14 data_chunk prepare_data()
    15 {
    16     return data_chunk();
    17 }
    18 
    19 void process(data_chunk&)
    20 {}
    21 
    22 bool is_last_chunk(data_chunk&)
    23 {
    24     return true;
    25 }
    26 
    27 std::mutex mut;
    28 std::queue<data_chunk> data_queue;
    29 std::condition_variable data_cond;
    30 
    31 void data_preparation_thread()
    32 {
    33     while(more_data_to_prepare())
    34     {
    35         data_chunk const data=prepare_data();
    36         std::lock_guard<std::mutex> lk(mut);
    37         data_queue.push(data);
    38         data_cond.notify_one();
    39     }
    40 }
    41 
    42 void data_processing_thread()
    43 {
    44     while(true)
    45     {
    46         std::unique_lock<std::mutex> lk(mut);
    47         data_cond.wait(lk,[]{return !data_queue.empty();});
    48         data_chunk data=data_queue.front();
    49         data_queue.pop();
    50         lk.unlock();
    51         process(data);
    52         if(is_last_chunk(data))
    53             break;
    54     }
    55 }
    56 
    57 int main()
    58 {
    59     std::thread t1(data_preparation_thread);
    60     std::thread t2(data_processing_thread);
    61     
    62     t1.join();
    63     t2.join();
    64 }

    示例代码演示了一个生产者data_preparation_thread和消费者data_processing_thread两个线程之间的同步。

    1. 生产者线程data_preparation_thread循环进行消息的准备,在37行往队列中插入一条记录后,通过38行的std::condition_variable实例唤醒一个等待线程。

    2. 消费者线程data_processing_thread在47行检查通过std::condition_variable.wait中的 lambda表达式检查队列是否为空,如果队列为空,wait()函数将解锁互斥量,并且将这个线程(上段提到的处理数据的线程)置于阻塞或等待状态。直到生产者线程data_preparation_thread有数据push到队列中后,通过std::condition_variable.notify_one()在38行进行通知唤醒。一旦被唤醒,消费者线程会再次获取互斥锁,并且对条件再次检查,并在条件满足的情况下,继续持有锁,并48行和以下部分的代码。

    3. std::condition_variable.wait等待条件满足是直接与等待条件相关的,而与通知到达无关,通知到达了,只是唤醒等待线程重新锁定共享数据,检查条件满足没有。wait的第一个参数为对共享数据进行保护的锁,只有在锁定状态下,wait才会去检查wait条件是否达成。

     线程安全队列代码示例:

    #include <queue>
    #include <mutex>
    #include <condition_variable>
    #include <memory>
    
    template<typename T>
    class threadsafe_queue
    {
    private:
        mutable std::mutex mut;
        std::queue<std::shared_ptr<T> > data_queue;
        std::condition_variable data_cond;
    public:
        threadsafe_queue()
        {}
    
        void wait_and_pop(T& value)
        {
            std::unique_lock<std::mutex> lk(mut);
            data_cond.wait(lk,[this]{return !data_queue.empty();});
            value=std::move(*data_queue.front());
            data_queue.pop();
        }
    
        bool try_pop(T& value)
        {
            std::lock_guard<std::mutex> lk(mut);
            if(data_queue.empty())
                return false;
            value=std::move(*data_queue.front());
            data_queue.pop();
        }
    
        std::shared_ptr<T> wait_and_pop()
        {
            std::unique_lock<std::mutex> lk(mut);
            data_cond.wait(lk,[this]{return !data_queue.empty();});
            std::shared_ptr<T> res=data_queue.front();
            data_queue.pop();
            return res;
        }
    
        std::shared_ptr<T> try_pop()
        {
            std::lock_guard<std::mutex> lk(mut);
            if(data_queue.empty())
                return std::shared_ptr<T>();
            std::shared_ptr<T> res=data_queue.front();
            data_queue.pop();
            return res;
        }
    
        bool empty() const
        {
            std::lock_guard<std::mutex> lk(mut);
            return data_queue.empty();
        }
    
        void push(T new_value)
        {
            std::shared_ptr<T> data(
                std::make_shared<T>(std::move(new_value)));
            std::lock_guard<std::mutex> lk(mut);
            data_queue.push(data);
            data_cond.notify_one();
        }
    
    };

    3. std::future<T>

    std::future是一个异步结果获得的对象,将任务与一个future对象关联,任务做完后就可以通过std::future对象得到结果,当我们需要时就可以随时去从std::future对象取得我们期待的结果.  异步需要执行的任务是通过provider来设定的,通过在定义provider的时候,将future与某个task关联在一起,就能通过future异步取得执行结果(假设fut为future对象,通过ful.get()取得结果)了。future也是一个模板类future<T>,T表示future返回结果的类型。

    通过thread在执行的任务是没有反获值的,如果需要获取线程执行的返回值,就需要通过future。std::future是一次性等待任务,只是执行一次,然后结果会返回给future,如果还来就需要重新设置。std:condition_variable是可以重复执行的。

    3.1 std::async()

    std::async启动一个异步任务并会返回一个std::future 对象,这个对象持有最终计算出来的结果。当你需要这个值时,你只需要调用这个futrue对象的get()成员函数;并且直到“期望”状态为就绪的情况下,线程才会阻塞;之后,返回计算结果。
    std::async的参数格式为std::async(parameA,paramB,paramC,paramD),参数说明如下:

    a) paramA是执行的方式,std::launch::deferred表示延迟执行,即在future上调用get()或者wait()才开始执行; std::launch::async表示立即启动一个新线程执行。   此参数忽略,如果忽略表示由具体实现选择方式。
    b) paramB是需要执行任务的函数名。              此参数为必选参数
    c) paramC表示,调用需要执行任务的函数的对象。   此参数为可选参数,因为可能不是一个类的成员函数,所以可以直接调用
    d) paramD表示函数的参数,所以D可以有很多项。

    如:
    #include <future>
    #include <iostream>
    #include <string>
    using namespace std;
    
    int find_the_answer_to_ltuae()
    {
        return 42;
    }
    
    void do_something_in_main_thread()
    { 
        std::cout<<"I'm in main thead
    ";
    }
    
    struct X
    {
        void foo(int inumber,std::string const& svalue)
        {
            cout<<"struct X inumber:"<<inumber<<"  svalue:"<<svalue<<endl;
        }
        std::string bar(std::string const& svalue)
        {
            cout<<"struct X svalue:" <<svalue<<endl;
        }
    };
    
    
    X x;
    auto f1=std::async(&X::foo,&x,42,"hello");
    auto f2=std::async(&X::bar,x,"goodbye");
    
    struct Y
    {
        double operator()(double value)
        {
            std::cout<<"struct Y :"<<value<<std::endl;
        }
    };
    Y y;
    auto f3=std::async(Y(),3);
    auto f4=std::async(std::ref(y),4);
    
    auto f6=std::async(std::launch::async,Y(),6); // 在新线程上执行
    auto f7=std::async(std::launch::deferred,Y(),7); // 在wait()或get()调用时执行
    // f7.wait() //// 调用延迟函数
    
    int main()
    {
        std::future<int> the_answer=std::async(find_the_answer_to_ltuae);
        do_something_in_main_thread();
        std::cout<<"The answer is "<<the_answer.get()<<std::endl;
    }

    输出如下:

    lenmom@M1701:~/workspace/open-source$ ./a.out
    struct X svalue:goodbyestruct Y :
    struct X inumber:42  svalue:hello
    3
    struct Y :4
    struct Y :6
    I'm in main thead
    The answer is 42

    3.2 std::packaged_task<>
    std::packaged_task<>将一个可调用对象(即需要执行的任务)或函数和一个future封装在一起的可调用对象。当构造一个 std::packaged_task<> 实例时,必须传入一个函数或可调用对象,这个函数或可调用的对象需要能接收指定的参数和返回可转换为指定返回类型的值。类型可以不完全匹配;比如使用float f(int )函数,来构建 std::packaged_task<double(double)> 的实例,因为在这里,类型可以隐式转换。使用std::packaged_task关联的std::future对象保存的数据类型是可调对象的返回结果类型,如示例函数的返回结果类型是int,那么声明为std::future<int>,而不是std::future<int(int)。代码示例:

    #include <iostream>
    #include <type_traits>
    #include <future>
    #include <thread>
    
    using namespace std;
    int main()
    {
        std::packaged_task<int()> task([]() {
            std::this_thread::sleep_for(std::chrono::seconds(5));// 线程睡眠5s
            return 4; });
        std::thread t1(std::ref(task));
        std::future<int> f1 = task.get_future();
        
        auto r = f1.get();// 线程外阻塞等待
        std::cout << r << std::endl;
    
        t1.join();
    return 0; }

    3.3 std::promise<>

     std::promise用来包装一个值将数据和future绑定起来,为获取线程函数中的某个值提供便利,需要显示设置promise的返回值,取值是间接通过promise内部提供的future来获取的,也就是说promise的层次比future高。

    #include <iostream>
    #include <type_traits>
    #include <future>
    #include <thread>
    
    using namespace std;
    int main()
    {
        std::promise<int> promiseParam;
        std::thread t([](std::promise<int>& p)
        {
            std::this_thread::sleep_for(std::chrono::seconds(10));// 线程睡眠10s
            p.set_value_at_thread_exit(4);//
        }, std::ref(promiseParam));
        std::future<int> futureParam = promiseParam.get_future();
    
        auto r = futureParam.get();// 线程外阻塞等待
        std::cout << r << std::endl;
     
        t.join()
    return 0; }
  • 相关阅读:
    【转】NHibernate主键类型介绍
    【转】NHibernate 各种数据库配置
    NHibernate常见错误
    NHibernate 设置主键不自增长
    Oracle 同名字段的该行数据按照创建时间最新的隐藏其他
    不用第三个变量就能交换两个变量值的五个方法
    Java IO 要点总结
    Java API 各个包的内容解释
    Ways to 优化JAVA程序设计和编码,提高JAVA性能
    Java菜鸟入坑学习要点
  • 原文地址:https://www.cnblogs.com/lenmom/p/10161799.html
Copyright © 2020-2023  润新知