《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风格)。此处略去,

posted on 2013-04-22 22:12  唐风思琪  阅读(2305)  评论(0编辑  收藏  举报

导航