• 《C++ concurrency in action》 读书笔记 -- Part 3 第四章 线程的同步


    唐风

    www.cnblogs.com/liyiwen


    《C++ concurreny in action》 第四章 Synchronizing concurrent operations

    这一章主要讲C++11中的进行线程同步的方法

    4.1 Waiting for an event or other condition

    经常遇到的一种场景是一个线程A需要等待另一个线程B完成一些“事”(操作)才能往下运行继续处理。

    有以下几种“办法”

    一,设置一个多个线程都能访问到的变量,B线程一旦完成自己的操作,就把这个全局的变量设为某个值,而线程A则 不断地去检查变量的值是不是已经设置为这个值,一直到满足条件才往下执行。否则就一直循环地Check这个变量。(当然,A和B都要通过互斥锁来访问变量)。这个方法一般来说都不行,A不断地执行检查变量值只是个纯粹浪费CPU处理时间的操作。而且由于A进行锁定的时候,B还不能去操作那个被锁定全局变量,也无形中增加了最终条件满足所需要的时间

    二,方法一的改进,就是A线程Sleep一段时间再检查一次

    bool flag;
    std::mutex m;
    void wait_for_flag()
    {
        std::unique_lock<std::mutex> lk(m);
        while(!flag) 
        {
            lk.unlock(); 
            std::this_thread::sleep_for(std::chrono::milliseconds(100)); 
            lk.lock(); 
        }
    }

    这个方法性能更好,但是A的sleep时间取多少并不很好判断,而且这个方法对于实时的或是要求快速反应的程序并不合并

    三,用事件来进行同步

    这是最“正常的”解法。C++11中提供了相应的库和函数来完成这种任务。

    其中最为常见的就是:condition variable(条件变量)。条件变量(condition variable)会把信号量(event)与某些条件在一起,如果一个线程把这个条件变量设置某满足某一条件时,condition variable就会自动地通知其它等待这个条件的线程。

    4.1.1 Waiting for a condition with condition variable

    C++11中有两种condition variable的实现:

    std::condition_variable 和 std::condition_variable_any (都定义在<condition_variable>头文件中)

    • std::condition_variable只能与mutex一起使用
    • std::condition_variable_any可以与mutext-like(只需要满足一些最低要求锁操作的“锁”,不仅限于mutex)一起使用,但是,可能会“更大更慢更占操作系统资源一些”

    std::condition_variable的使用例:

    std::mutex mut;
    std::queue<data_chunk> data_queue; 
    std::condition_variable data_cond;
    void data_preparation_thread()
    {
        while(more_data_to_prepare())
        {
            data_chunk const data=prepare_data();
            std::lock_guard<std::mutex> lk(mut);
            data_queue.push(data); 
            data_cond.notify_one(); 
        }
    }
    void data_processing_thread()
    {
        while(true)
        {
            std::unique_lock<std::mutex> lk(mut);    // (1)
            data_cond.wait(lk,[]{return !data_queue.empty();}); 
            data_chunk data=data_queue.front();
            data_queue.pop();
            lk.unlock(); 
            process(data);
            if(is_last_chunk(data))
                break;
        }
    }

    注意(1)必须使用 unique_lock,而不能用guard_lock,因为在condition_variable内部实现中,有可能要多次进行lock和unlock操作。

    4.1.2

    因为线程间的共享数据的queue非常之有用,而上面的例子并不是一个很好的用法,本节构造了一个好的“线程间共享queue”。

    4.2 Waiting for one-off events with futures

    one-off event就是“只需要等一次的事件”。C++11对这种场景提供了 future 模型。如果一个线程需要等待一个“只会发生一次的”事件,它就可以获取一个包含这个 event 的 future。别的线程可以把 future 设置为 ready 状态,这样等待 future的线程就可以知晓并接着进行处理。future一旦ready就不能再重新设置了(不会再变成 unready状态了)

    C++标准库中有两种future,

    • unique future -> std::future<>
    • shared_future -> std::shared_future<>

    是模仿智能指针 std::unique_ptr 和 std::share_ptr 的思路来分类的。

    最常见的一种one-off event的场景就是开启一个线程进行后台的计算,而主线程去做些别的事,之后再等这个计算的结果出来。当从第二章我们知道,std::thread 没有提供什么方便的方法可以让你取得线程调用函数的“返回值”。这种场景的一个方案就是:std::async

    4.2.1 从后台线程中返回值

    std::async和“线程”几乎有同样的作用,但是它会返回一个future,这个future就带有线程函数的返回值。你可以用future.get()来获取这个值,get()函数会在线程结果之前一直阻塞。

    #include <future>
    #include <iostream>
    int find_the_answer_to_ltuae();
    void do_other_stuff();
    int main()
    {
        std::future<int> the_answer=std::async(find_the_answer_to_ltuae);
        do_other_stuff();
        std::cout<<"The answer is "<<the_answer.get()<<std::endl;
    }

    std::async像std::thread一样,可以在构造的时候给线程主函数进行传参。本节比较详细地说明了传参的方法(主要是:如何传引用和如何move)

    struct X
    {
        void foo(int,std::string const&);
        std::string bar(std::string const&);
    };
    X x;
    auto f1=std::async(&X::foo,&x,42,"hello"); 
    auto f2=std::async(&X::bar,x,"goodbye"); 
    struct Y
    {
        double operator()(double);
    };
    Y y;
    auto f3=std::async(Y(),3.141); 
    auto f4=std::async(std::ref(y),2.718); 
    X baz(X&);
    std::async(baz,std::ref(x)); 
    class move_only
    {
    public:
        move_only();
        move_only(move_only&&)
        move_only(move_only const&) = delete;
        move_only& operator=(move_only&&);
        move_only& operator=(move_only const&) = delete;
        void operator()();
    };
    auto f5=std::async(move_only());

    std::async一构造完就会启动线程函数,还是要等future.get()被调用才启动线程函数,这是取决是编译器提供商的实现的(标准并没有进行规定)。但如果你想确认它,你可以指定参数:std::launch(立即启动新线程) 或是 std::deferred(等到wait或是get函数被调用时才启动新线程)。

    auto f6=std::async(std::launch::async,Y(),1.2); 
    auto f7=std::async(std::launch::deferred,baz,std::ref(x)); 
    auto f8=std::async( 
    std::launch::deferred | std::launch::async,
    baz,std::ref(x));
    auto f9=std::async(baz,std::ref(x)); 
    f7.wait();

    将future与并行任务关联的方法除了使用 std::async之外,还可以使用 std::packaged_task<>,或是std::promise<>,其中std::packaged_task是std::promise的高层一些的抽象。下节就讲这个。

    4.2.2 Associating a task with a future

    std::packaged_task<>可以把任务(Task,函数或是函数对象之类可调用的东西)与future关联起来,当std::packaged_task被调用的时候,它就会调用自己所包装的任务,并在可调用对象返回时把值设置到future中,然后再把future设定为ture。这个可以用在构造线程池时使用(第九章)会讲这个,也可以用在其它的任务管理调度。

    std::packaged_task<>的模板参数与std::function的类似,是可调用对象的类型签名(像void(),int(std::string&, double*)之类的),但参数部分是用来进行并行任务的参数列表,而返回值部分则是可从future中get到的值的类型

    template<>
    class packaged_task<std::string(std::vector<char>*,int)>
    {
    public:
    template<typename Callable>
    explicit packaged_task(Callable&& f);
    std::future<std::string> get_future();
    void operator()(std::vector<char>*,int);
    };

    std::packaged_task是个可调用的对象,因此它也可以被封装在std::function中,或是使用在std::thread中表示一个线程的执行函数,或是在别的函数中调用,调用完之后它会把返回值作为异步的结果放在所关联的std::future中。所以使用时,我们可以把一个需要执行的任务包装在std::packaged_task中,然后获取它的std::future后,把这个包装好的任务传递给其它的线程。等我们需要这个任务的返回值的时候,我们就可以等待future变成ready。

    下面是一个GUI的例子:

    #include <deque>
    #include <mutex>
    #include <future>
    #include <thread>
    #include <utility>
    
    std::mutex m;
    std::deque<std::packaged_task<void()> > tasks;
    bool gui_shutdown_message_received();
    void get_and_process_gui_message();
    
    void gui_thread() 
    {
        while(!gui_shutdown_message_received()) 
        {
            get_and_process_gui_message(); 
            std::packaged_task<void()> task;
            {
                std::lock_guard<std::mutex> lk(m);
                if(tasks.empty()) 
                continue;
                task=std::move(tasks.front()); 
                tasks.pop_front();
            }
            task(); 
        }
    }
    
    std::thread gui_bg_thread(gui_thread);
    template<typename Func>
    
    std::future<void> post_task_for_gui_thread(Func f)
    {
        std::packaged_task<void()> task(f); 
        std::future<void> res=task.get_future(); 
        std::lock_guard<std::mutex> lk(m);
        tasks.push_back(std::move(task)); 
        return res; 
    }

    《C++ concurreny in action》 第四章 Synchronizing concurrent operations

    4.2.3 Makeing (std::)promise

    但是有时候我们在异步地获取值的时候,并不能总是“获取一个可调用对象的返回值”这么简单,在这些场景下,我们不一定能包装成一个可调用对象,或是,我们需要在一个函数获取多种不同类型的返回,这时std::packaged_task就不好用了。这种情况下我们可以使用std::promise。总的来说,std::promise/std::future对与std::packaged_task/std::future对的关系是类似的。我们也可以从一个promise关联获取一个std::future,当调用std::promise的set_value成员函数时,它所关联的std::future的值就会被设定,而且成为ready状态。

    下面是一个例子:

    #include <future>
    
    void process_connections(connection_set& connections)
    {
        while(!done(connections)) 
        {
            for(connection_iterator connection=connections.begin(),end=connections.end();
                connection!=end;
                ++connection)
            {
                if(connection->has_incoming_data()) 
                {
                    data_packet data=connection->incoming();
                    std::promise<payload_type>& p=
                    connection->get_promise(data.id); 
                    p.set_value(data.payload);
                }
                if(connection->has_outgoing_data()) 
                {
                    outgoing_packet data=
                    connection->top_of_outgoing_queue();
                    connection->send(data.payload);
                    data.promise.set_value(true); 
                }
            }
        }
    }

    4.2.4 Saving an exception for the future

    在上面的例子中,我们都没有提到出现异常的情况。虽然 std::future与std::async/std::packaged_task/std::promise的场景大部分是多线中,但标准仍然提供了一个像“单线程环境”中的异常那样比较“符合”我们想像的处理方法:当std::async/std::packaged_task/std::promise发生异常时,可以把异常保存在std::future当中,等另一个线程调用 std::future和get/wait函数时,再把这个异常重新抛出(这时抛出的是原来异常的引用还是拷贝,则取决于编译器的实现)。std::async/std::packaged_task的情况下,我们不需要做额外的处理,这一切库函数已经做好了,std::promise的情况下,为了抛出异常,我们要则要调用 set_exception而不是set_value

    extern std::promise<double> some_promise;
    try
    {
        some_promise.set_value(calculate_value());
    }
    catch(...)
    {
        some_promise.set_exception(std::current_exception());
    }

    另外,如果std::promise的set_value函数没有被调用,或是std::packaged_task没有被调用就被析构了的话,他们也会把异常(std::future_error,值为std::future_errc::broken_promise)存在std::future中,并把future设置为ready。

    4.2.5 Waiting from multiple threads

    std::future的资源占有方式是unique的,只能在一个线程中获取(moveable)。如果有多个线程需要获取同一个future的话,必须使用std::shared_future(copyable)。

    std::promise<std::string> p;

    std::shared_future<std::string> sf(p.get_future());

    clip_image001

    4.3 Waiting for a time limit

    前面介绍的所以有阻塞的调用“永远等待”的,不会超时。如果要设置超时,需要另外指定。

    大体上分,C++11中有两种设置超时的方法,一种是设置超时的期间(duration-based),也就是从调用起多长时间超时(一般都是_for后缀),另一种是设置超时的绝对时间点(absolute timeout,一般是_util后缀)。

    4.3.1 Clocks

    C++中处理时间一般可以使用clock,clock可以提供下面4项内容

    The time now

    ■ The type of the value used to represent the times obtained from the clock

    ■ The tick period of the clock

    ■ Whether or not the clock ticks at a uniform rate and is thus considered to be a steadyclock

    静态函数now()可以获得现在的时间。比如std::chrono::system_clock::now()可以获取现在的系统时钟的时间。

    tick period是以秒的分数形式指定的,std::ratio<1,25>就是1秒跳25次,std::ratio<5,2>就是2.5秒跳一次

    4.3.2 Duration

    时间段用std::chrono::duration来表示,这个类有两个模板参数,第一个表示时间的类型(int, long, double),第二个参数是一个“分数”,表示了一个单位的duration对象是多少秒,比如

    std::chrono::duration<short, std::ratio<60, 1>> 就是60秒

    std::chrono::duration<double,std::ratio<1,1000>> 是1/1000秒

    标准库定义了一些duration的typedef:

    nanoseconds, microseconds, milliseconds, seconds, minutes, hours

    duration之间是可以隐式转换和进行算术运算的(此处不详记)

    4.3.3 Time point

    std::chrono::time_point<>表示一个时间点。

    接受两个模板参数,第一个模板参数表示使用的“时钟”,第二个模板参数表示“计量的单位”(用 std::chrono::duration<>来指定)

    比如:

    std::chrono::time_point<std::chrono::system_clock, std::chrono::minutes> 就是指使用系统时钟进行,并以分为单位测定的时间点。std::chrono::time_point是可以与std::chrono::duration进行运算而得到新的std::chrono::time_point的。

    auto start=std::chrono::high_resolution_clock::now();
    do_something();
    auto stop=std::chrono::high_resolution_clock::now();
    std::cout<<”do_something() took “
            <<std::chrono::duration<double,std::chrono::seconds>(stop-start).count()
            <<” seconds”<<std::endl;

    4.3.4 Functions that accept timeouts

    线程库中可以指定Timeout的函数如下表

    clip_image002

    4.4 主要是讲如何使用前章和本章中介绍的技术来简化C++的代码(模拟一些Function Programming风格)。此处略去,

    ---- 总会有一个人需要你的分享~! 唐风: www.cnblogs.com/muxue ------
  • 相关阅读:
    OI 复赛注意事项整理
    U138415 堆箱子 | 扩展欧几里得 扩欧
    扩欧-扩展欧几里得 | 数论学习笔记
    U138097 小鱼吃大鱼 埃氏筛
    牛客1029A 袜子分配
    U137971 公司搬迁
    初赛知识点整理
    SQL注入技术
    写出易于调试的SQL
    dos命令大全
  • 原文地址:https://www.cnblogs.com/muxue/p/3036731.html
Copyright © 2020-2023  润新知