C++11并发学习之四:线程同步(续)

来源:互联网 发布:mysql 修改系统参数 编辑:程序博客网 时间:2024/05/16 07:03

有时候,在第一个线程完成前,可能需要等待另一个线程执行完成。C++标准库提供了一些工具可用于这种同步操作,形式上表现为条件变量(condition variable)和期望(future)。


一.条件变量(condition variable)
C++标准库对条件变量有两套实现:std::condition_variable和std::condition_variable_any。这两个实现都包含在<condition_variable> 头文件的声明中。两者都需要与一个互斥量一起才能工作(互斥量是为了同步);前者仅限于与std::mutex一起工作,而后者可以和任何满足最低标准的互斥量一起工作,从而加上了_any的后缀。因为std::condition_variable_any更加通用,这就可能从体积、性能,以及系统资源的使用方面产生额外的开销,所以std::condition_variable一般作为首选的类型,当对灵活性有硬性要求时,我们才会去考std::condition_variable_any。

下例使用std::condition_variable去处理之前提到的情况——当有数据需要处理时,如何唤醒休眠中的线程对其进行处理。

#include <thread>#include <iostream>#include <string>#include <chrono>#include <assert.h>#include <mutex>#include <condition_variable>#include <queue>int counter=0;std::mutex mtx;std::queue<int> dataQuene;std::condition_variable dataCondition;void func_preparation(){    for (int i=0; i<10; ++i)    {        std::unique_lock<std::mutex> lck(mtx);        ++counter;        dataQuene.push(counter);        dataCondition.notify_one();    }}void func_processing(){    while (true)    {        std::unique_lock<std::mutex> lck(mtx);        dataCondition.wait(lck,[]{return !dataQuene.empty();});        int num=dataQuene.front();        std::cout<<num<<std::endl;        dataQuene.pop();        lck.unlock();    }}int main(){    std::thread workerThreadPreparation(func_preparation);    workerThreadPreparation.join();    std::thread workerThreadProcessing(func_processing);    workerThreadProcessing.join();    return 0;}

workerThreadPreparation线程将数据存储到队列中,然后调用std::condition_variable的notify_one()成员函数,对等待的线程(这里为workerThreadProcessing线程)进行通知。这里为workerThreadProcessing线程收到通知后,会调用std::condition_variable的成员函数wait(),判断队列是否为空,如果队列不为空,则取出数据并打印。

需要注意的是,wait的第二个参数使用了lambda表达式,关于lambda详见:

每个C++开发者都应该使用的十个C++11特性。

运行效果如下图所示。



二.期望(future)

C++标准库模型将某种一次性事件称为“期望” (future)。当一个线程需要等待一个特定的一次性事件时,在某种程度上来说它就需要知道这个事件在未来的表现形式。之后,这个线程会周期性(较短的周期)的等待或检查,事件是否触发;在检查期间也会执行其他任务。另外,在等待任务期间它可以先执行另外一些任务,直到对应的任务触发,而后等待期望的状态会变为“就绪”(ready)。当事件发生时(并且期望状态为就绪),这个“期望”就不能被重置。在C++标准库中,有两种“期望”,使用两种类型模板实现,声明在头文件中: 唯一期望(uniquefutures)( std::future<> )和共享期望(shared futures)( std::shared_future<> )。这是仿照 std::unique_ptr 和 std::shared_ptr 。 std::future 的实例只能与一个指定事件相关联,而 std::shared_future 的实例就能关联多个事件。虽然,我希望用于线程间的通讯,但是“期望”对象本身并不提供同步访问。当多个线程需要访问一个独立“期望”对象时,他们必须使用互斥量或类似同步机制对访问进行保护。

假如有一个耗时的计算,可以启动一个新线程来执行这个计算,但是这就意味着必须关注如何传回计算的结果。因为std::thread并不提供直接接收返回值的机制,这里就需要std::async函数模板了。当任务的结果你不着急要时,你可以使用 std::async 启动一个异步任务。与 std::thread 对象等待运行方式的不同, std::async 会返回一个 std::future 对象,这个对象持有最终计算出来的结果。当你需要这个值时,你只需要调用这个对象的get()成员函数;并且直到“期望”状态为就绪的情况下,线程才会阻塞;之后,返回计算结果。

std::future 通常由某个Provider创建,你可以把Provider想象成一个异步任务的提供者,Provider在某个线程中设置共享状态的值,与该共享状态相关联的std::future对象调用 get(通常在另外一个线程中) 获取该值,如果共享状态的标志不为ready,则调用std::future::get会阻塞当前的调用者,直到Provider设置了共享状态的值(此时共享状态的标志变为 ready),std::future::get返回异步任务的值或异常(如果发生了异常)。
一个有效(valid)的std::future对象通常由以下三种Provider创建,并和某个共享状态相关联。Provider可以是函数或者类,他们分别是:
☆std::async 函数。
☆std::promise::get_future,get_future 为promise类的成员函数。
☆std::packaged_task::get_future,此时get_future为packaged_task的成员函数。

#include <thread>#include <iostream>#include <string.h>#include <chrono>#include <assert.h>#include <future>//求校验和int getCheckSum( char *data, int data_length ){    int  check_sum = 0;    while ( --data_length >= 0 )    {        check_sum += *data++;    }    return check_sum;}int time_consuming_task(){    std::cout<<"worker thread ID:"<<std::this_thread::get_id()<<std::endl;    char * str="can ge ge blog";    return getCheckSum(str,strlen(str));}void do_other_task(){    std::cout<<"I am other task"<<std::endl;}int main(){    std::cout<<"main thread ID:"<<std::this_thread::get_id()<<std::endl;    std::future<int> result=std::async(time_consuming_task);    do_other_task();    std::cout<<"The result is "<<result.get()<<std::endl;    //防止窗口一闪而过    system("pause");    return 0;}

运行结果如下图所示。

虽然能得到正确结果,但是从线程ID可以看出time_consuming_task并未在新线程中运行。

现在来看看std::async的原型async(std::launch::async | std::launch::deferred, f, args...),第一个参数是线程的创建策略,有两种策略:
std::launch::async:在调用async就开始创建线程。
std::launch::deferred:延迟加载方式创建线程。调用async时不创建线程,直到调用了future的get或者wait时才创建线程。
第二个参数是线程函数,第三个参数是线程函数的参数。

因此只需将上述代码中的

std::future<int> result=std::async(time_consuming_task)

改为

std::future<int> result=std::async(std::launch::async,time_consuming_task)

运行结果如下所示。


此时,time_consuming_task在新线程中运行了。


三.承诺(promise)

promise对象可以保存某一类型T的值,该值可被future对象读取(可能在另外一个线程中),因此promise也提供了一种线程同步的手段。在promise对象构造时可以和一个共享状态(通常是std::future)相关联,并可以在相关联的共享状态(std::future)上保存一个类型为T的值。可以通过get_future来获取与该promise对象相关联的future对象,调用该函数之后,两个对象共享相同的共享状态(shared state)
☆promise对象是异步Provider,它可以在某一时刻设置共享状态的值。
☆future对象可以异步返回共享状态的值,或者在必要的情况下阻塞调用者并等待共享状态标志变为ready,然后才能获取共享状态的值。

#include <thread>#include <iostream>#include <string.h>#include <chrono>#include <assert.h>#include <future>int print_int(std::future<int>& fut){    std::cout<<"worker thread ID:"<<std::this_thread::get_id()<<std::endl;    int x = fut.get();//获取共享状态的值.    std::cout << "value: " << x << std::endl;//打印value:10}int main(){    std::cout<<"main thread ID:"<<std::this_thread::get_id()<<std::endl;    std::promise<int> prom;//生成一个std::promise<int>对象    std::future<int> fut = prom.get_future();//和future关联    std::thread t(print_int, std::ref(fut)); //将future交给另外一个线程t    prom.set_value(10);//设置共享状态的值,此处和线程t保持同步    t.join();    return 0;}



四.包装任务(packaged_task)

std::packaged_task包装一个可调用的对象,并且允许异步获取该可调用对象产生的结果。td::packaged_task将其包装的可调用对象的执行结果传递给一个std::future对象(该对象通常在另外一个线程中获取 std::packaged_task 任务的执行结果)。
td::packaged_task对象内部包含了两个最基本元素,一、被包装的任务(stored task),任务(task)是一个可调用的对象,如函数指针、成员函数指针或者函数对象,二、共享状态(shared state),用于保存任务的返回值,可以通过 std::future对象来达到异步访问共享状态的效果。
可以通过 std::packged_task::get_future来获取与共享状态相关联的 std::future对象。在调用该函数之后,两个对象共享相同的共享状态,具体解释如下:
☆std::packaged_task对象是异步 Provider,它在某一时刻通过调用被包装的任务来设置共享状态的值。
☆std::future对象是一个异步返回对象,通过它可以获得共享状态的值,当然在必要的时候需要等待共享状态标志变为ready。
std::packaged_task的共享状态的生命周期一直持续到最后一个与之相关联的对象被释放或者销毁为止。

#include <thread>#include <iostream>#include <string.h>#include <chrono>#include <assert.h>#include <future>int print_int(int from,int to){    std::cout<<"worker thread ID:"<<std::this_thread::get_id()<<std::endl;    for(int i=from;i<to;i++)    {        std::cout << i << std::endl;        std::this_thread::sleep_for(std::chrono::seconds(1));    }    std::cout << "Finished!" <<std::endl;    return to - from;}int main(){    std::packaged_task<int(int,int)> task(print_int);//设置packaged_task    std::future<int> fu = task.get_future();//获得与packaged_task共享状态相关联的future对象    std::thread t(std::move(task), 0, 10);//创建一个新线程完成计数任务,std::move用于无条件的将其参数转换为右值    int value = fu.get();//等待任务完成并获取结果    std::cout << "The print_int lasted for " << value << " seconds"<<std::endl;    t.join();    return 0;}





3 0