C++ 多线程编程实战11:std::async

C++ 多线程:std::async

概念

我们之前的文章介绍过了std::thread,它使得我们可以非常方便的创建线程,执行异步任务,它的基本用法是这样的:

#include <iostream>

int add(int a, int b) { return a + b;}

int main() {
    std::thread t(add, 2, 3);
    t.join();
}

上面这段代码新创建一个线程来计算两个数相加。但我们没办法获取线程的计算返回值。有时候使用thread有些不便,比如我希望获取线程函数的返回结果的时候,我就不能直接通过 thread.join()得到结果,这时就必须定义一个变量,在线程函数中去给这个变量赋值,然后join,最后得到结果,这个过程是比较繁琐的。 为此,cpp11提供了异步接口std::async,通过这个异步接口可以很方便的获取线程函数的执行结果。std::async会自动创建一个线程去调用线程函数,它返回一个std::future,这个future中存储了线程函数返回的结果,当我们需要线程函数的结果时,直接从future中获取,非常方便。但是std::async提供的便利不仅仅是这一点,它首先解耦了线程的创建和执行,使得我们可以在需要的时候获取异步操作的结果;其次它还提供了线程的创建策略(比如可以通过延迟加载的方式去创建线程),使得我们可以以多种方式去创建线程。下面我们就来详细学习一下std::async

std::async

std::async能够简单的使用可用的硬件并行来运行自身包含的异步任务。当调用std::async返回一个包含任务结果的std::future对象。根据策略,任务在其所在线程上是异步运行的,当有线程调用了这个future对象的wait()get()成员函数,则该任务会同步运行。有点类似封装了threadpackged_task的功能,使异步执行一个任务更为方便。

声明

enum class launch
{
  	async,  // 运行新线程来执行任务
    deferred   // 惰性求值,请求结果时才执行任务
};

template<typename Callable,typename ... Args>
future<result_of<Callable(Args...)>::type>
async(Callable&& func, Args&& ... args);

template<typename Callable,typename ... Args>
future<result_of<Callable(Args...)>::type>
async(launch policy, Callable&& func, Args&& ... args);

std::async是一个函数而非类模板,其函数执行完后的返回值绑定给std::futrue对象。func是要调用的可调用对象(function, member function, function object, lambda),Args是传递给Func的参数,std::launch policy是启动策略,它控制std::async的异步行为,我们可以用三种不同的启动策略来创建std::async

  • std::launch::async参数 保证异步行为,即传递函数将在单独的线程中执行;
  • std::launch::deferred参数 当其他线程调用get()/wait()来访问共享状态时,将调用非异步行为;
  • std::launch::async | std::launch::deferred参数 是默认行为(可省略)。有了这个启动策略,它可以异步运行或不运行,这取决于系统的负载。
#include <future>
#include <iostream>
#include <thread>
#include <chrono>

int entry() {
    std::cout <<"call entry" << std::endl;
    return 11;
}
int main() {
    std::future<int> the_answer=std::async(entry);
    std::this_thread::sleep_for(std::chrono::seconds(1));
    std::cout << "The answer is " << the_answer.get()<<std::endl;
}

std::thread方式一样,std::async允许通过添加额外的调用参数,向函数传递额外的参数。第一个参数是指向成员函数的指针,第二个参数提供这个函数成员类的具体对象(是通过指针,也可以包装在std::ref中),剩余的参数可作为函数的参数传入。否则,第二个和随后的参数将作为函数的参数,或作为指定可调用对象的第一个参数。和std::thread一样,当参数为右值时,拷贝操作将使用移动的方式转移原始数据,就可以使用“只移动”类型作为函数对象和参数。

#include <iostream>
#include <thread>
#include <chrono>
#include <string>
#include <future>

struct X
{
    void foo(int x, std::string const& str) {
        std::cout << "foo: " << x << " " << str <<std::endl;
    }
    std::string bar(std::string const& str) { 
        std::cout << "bar: " << str <<std::endl;
        return str;
    }
};
void test_1() {
    std::cout << "test_1" <<std::endl;
    X x;
    auto f1=std::async(&X::foo,&x, 42, "hello");  // 调用p->foo(42, "hello"),p是指向x的指针
    auto f2=std::async(&X::bar, x, "goodbye");  // 调用tmpx.bar("goodbye"), tmpx是x的拷贝副本
}

struct Y
{
  double operator()(double x) { return x; }
};
void test_2() {
    std::cout << "test_2" <<std::endl;
    Y y;
    auto f3=std::async(Y(), 3.141);  // 调用tmpy(3.141),tmpy通过Y的移动构造函数得到
    std::cout << "test_2 f3: " << f3.get() <<std::endl;
    auto f4=std::async(std::ref(y), 2.718);  // 调用y(2.718)
    std::cout << "test_2 f4: " << f4.get() <<std::endl;
}

X baz(X&) { return X(); }

void test_3() {
    std::cout << "test_3" <<std::endl;
    X x;
    auto f=std::async(baz,std::ref(x));  // 调用baz(x)
    f.get();
}

class move_only
{
public:
  move_only() {std::cout << "default\n";}
  move_only(move_only&&) {std::cout << "copy\n";}
  move_only(move_only const&) = delete;
  move_only& operator=(move_only&&) { std::cout << "operator=\n";return *this; }
  move_only& operator=(move_only const&) = delete;
  
  void operator()() {std::cout << "operator()\n";}
};

void test_4() {
    std::cout << "test_4" <<std::endl;
    // move_only()
    // 然后是std::move(move_only())构造得到
    // 最后是operator()
    auto f5=std::async(move_only());
}

int main() {
    test_1();
    test_2();
    test_3();
    test_4();
    std::cout << "finsh!" <<std::endl;
}

下面我们也看下带policy情况:

#include <iostream>
#include <thread>
#include <chrono>
#include <string>
#include <future>

struct X
{
    void foo(int x, std::string const& str) {
        std::cout << "foo: " << x << " " << str << std::endl;
    }
    std::string bar(std::string const& str) { 
        std::cout << "bar: " << str << std::endl;
        return str;
    }
};

struct Y
{
  double operator()(double x) { 
      std::cout << "Y operator(): " << x << std::endl;
      return x; }
};

X baz(X&) {
    std::cout << "call baz()" << std::endl;
    return X(); 
    }

int main() {
    // 在新线程上执行
    auto f6=std::async(std::launch::async, Y(), 1.2);

    X x;
    // 在wait()或get()调用时执行
    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();  //  调用延迟函数

    std::cout << "finsh!" <<std::endl;
}

注意事项

注意1 返回值的生存周期

std::async 的返回值(std::future)在析构函数里会等待任务完成,如果不注意 std::async 的返回值很有可能就没有异步效果,看下原文是这样说的:

If the std::future obtained from std::async is not moved from or bound to a reference, the destructor of the std::future will block at the end of the full expression until the asynchronous operation completes, essentially making code such as the following synchronous:

std::async(std::launch::async, []{ f(); }); // temporary’s dtor waits for f() std::async(std::launch::async, []{ g(); }); // does not start until f() completes

#include <iostream>
#include <chrono>
#include <future>

std::time_t now() 
{
    auto t0 = std::chrono::system_clock::now();
    std::time_t time_t_today = std::chrono::system_clock::to_time_t(t0);
    return time_t_today;  // seconds
}

void print() {
    std::cout << now() << " print start!" << std::endl;
    std::this_thread::sleep_for(std::chrono::seconds(1));
    std::cout << now() << " print end!" << std::endl;
}

int main() 
{
     

        auto f = std::async(std::launch::async, print);
        auto j = std::async(std::launch::async, print);
        auto a = std::async(std::launch::async, print);
        auto b = std::async(std::launch::async, print);
        auto x = std::async(std::launch::async, print);
        auto e = std::async(std::launch::async, print);
        auto g = std::async(std::launch::async, print);
        std::cout << now() << " all finsh1" << std::endl;
}

输出:

输出结果
       
    1674742668 print start!
    1674742668 all finsh1
    1674742668 print start!
    1674742668 print start!
    1674742668 print start!
    1674742668 print start!
    1674742668 print start!
    1674742668 print start!
    1674742669 print end!
    1674742669 print end!
    1674742669 print end!
    1674742669 print end!
    1674742669 print end!
    1674742669 print end!
    1674742669 print end!

可以看到,他们是并行运行的。
但把main中的代码换成如下的代码



    {
        auto f = std::async(std::launch::async, print);
    }//wait the print finish here   std::async 的返回值(std::future)在析构函数里会等待任务完成
    {
        auto j = std::async(std::launch::async, print);
    }//wait the print finish here   std::async 的返回值(std::future)在析构函数里会等待任务完成
    {
        auto a = std::async(std::launch::async, print);
    }//wait the print finish here   std::async 的返回值(std::future)在析构函数里会等待任务完成
    {
        auto b = std::async(std::launch::async, print);
    }//wait the print finish here   std::async 的返回值(std::future)在析构函数里会等待任务完成
    {

        auto x = std::async(std::launch::async, print);
    }//wait the print finish here  std::async 的返回值(std::future)在析构函数里会等待任务完成

    {
        auto e = std::async(std::launch::async, print);
    }//wait the print finish here  std::async 的返回值(std::future)在析构函数里会等待任务完成
    {
        auto g = std::async(std::launch::async, print);
    }//wait the print finish here  std::async 的返回值(std::future)在析构函数里会等待任务完成
    {

        std::cout << now() << " all finshed" << std::endl;
    }//wait the print finish here   std::async 的返回值(std::future)在析构函数里会等待任务完成
  • 输出结果
1674743501 print start!
1674743502 print end!
1674743502 print start!
1674743503 print end!
1674743503 print start!
1674743504 print end!
1674743504 print start!
1674743505 print end!
1674743505 print start!
1674743506 print end!
1674743506 print start!
1674743507 print end!
1674743507 print start!
1674743508 print end!
1674743508 all finshed

  • std::async的返回值(std::future)在析构函数里会等待任务完成,所以要让创建的async的对象在同一生命周期内,才能保证他们的并发。

注意2:默认policy

使用默认策略却很方便,它不需要你显示指定的,或者你可以显示指定为std::launch::async | std::launch::deferred,cpp标准中给出的说明是:进行异步执行还是惰性求值取决于实现:

auto future = std::async(func);        // 使用默认发射模式执行func

这种调度策略我们没有办法预知函数func是否会在哪个线程执行,甚至无法预知会不会被执行,因为func可能会被调度为推迟执行,即调用get或wait的时候执行,而get或wait是否会被执行或者在哪个线程执行都无法预知。

同时这种调度策略的灵活性还会混淆使用thread_local变量,这意味着如果func写或读这种线程本地存储(Thread Local Storage,TLS),预知取到哪个线程的本地变量是不可能的。

它也影响了基于wait循环中的超时情况,因为调度策略可能为deferred的,调用wait_for或者wait_until会返回值std::launch::deferred。这意味着下面的循环,看起来最终会停止,但是,实际上可能会一直运行:

#include <iostream>
#include <chrono>
#include <future>

std::time_t now() 
{
    auto t0 = std::chrono::system_clock::now();
    std::time_t time_t_today = std::chrono::system_clock::to_time_t(t0);
    return time_t_today;  // seconds
}

void print() {
    std::cout << now() << " print start!" << std::endl;
    std::this_thread::sleep_for(std::chrono::seconds(2));
    std::cout << now() << " print end!" << std::endl;
}


int main() {
    // auto future = std::async(print);  // (概念上)异步执行f
    auto future = std::async(std::launch::deferred, print);  // 模拟默认policy时系统选择deferred的情况

    // 系统选择policy是deferred,那么下面这个循环将一直进行不会退出
    while(future.wait_for(std::chrono::seconds(1)) != std::future_status::ready)
    {
        std::cout << now() << " no ready!" <<std::endl;
    }

    std::cout << now() << " all finsh!" <<std::endl;
}

为避免陷入死循环,我们必须检查future是否把任务推迟,然而future无法获知任务是否被推迟,一个好的技巧就是通过wait_for(0)来获取future_status是否是deferred,或者用do while里面把status全部进行判断做单独处理:

使用wait_for(0)
如果一直都是默认policy时系统选择deferred
#include <iostream>
#include <chrono>
#include <future>
#include <thread>

std::time_t now()
{
    auto t0 = std::chrono::system_clock::now();
    std::time_t time_t_today = std::chrono::system_clock::to_time_t(t0);
    return time_t_today; // seconds
}

int print()
{
    std::cout << now() << " print start!" << std::this_thread::get_id() << std::endl;
    std::this_thread::sleep_for(std::chrono::seconds(7));
    std::cout << now() << " print end!" << std::endl;

    return 999;
}

int main()
{
    std::cout << "main thread" << std::this_thread::get_id() << std::endl;
    // auto future = std::async(print); // (概念上)异步执行f
    auto future = std::async(std::launch::deferred, print); // 模拟默认policy时系统选择deferred的情况
 
  
    while (future.wait_for(std::chrono::seconds(2)) != std::future_status::ready) //
    {

        // 等待0s 相当于没有使用chrono执行阻塞线程启动,而future_status是否是deferre,才会影响线程是否阻塞启动。
        if (future.wait_for(std::chrono::seconds(0)) == std::future_status::deferred) // 线程没有执行直接跳出循环,get 启动线程,阻塞到线程完成
        {

            std::cout << "deferred" << std::endl;
            break; // 防止系统一直选择 默认policy时系统选择deferred的情况
        }
        else if (future.wait_for(std::chrono::seconds(0)) == std::future_status::timeout) // timeout的情况就重复执行while 直到ready状态
        {
            std::cout << " timeout " << std::endl;
        }

        else if (future.wait_for(std::chrono::seconds(0)) == std::future_status::ready)
        {
            std::cout << "main: result is ready" << std::endl; // 线程退出,ready,打印这句
        }
    }// status为  deferred 就跳出循环(依赖后面的get启动线程), status 为time_out 就重复while 循环直到ready, status为ready 就跳出while(说明线程启动完成)
    std::cout << future.get()<<std::endl; //使用get 启动线程, 阻塞执行到执行完毕
    std::cout << now() << " all finsh!" << std::endl;


  • 输出内容

main thread1
deferred       
1674824384 print start!1
1674824391 print end!
999
1674824391 all finsh!
如果一直都是默认policy时系统选择deferred,但是不调用get,那线程将不会调用
// std::cout << future.get()<<std::endl; 
  • 输出结果

main thread1
deferred
1674824662 all finsh!
如果一直都是默认policy时系统选择async

  auto future = std::async(print); // (概念上)异步执行f
    // auto future = std::async(std::launch::deferred, print); // 模拟默认policy时系统选择deferred的情况

  • 输出结果

main thread1
1674825162 print start!2
 timeout
 timeout
 timeout 
1674825169 print end!
1674825169 all finsh!
使用do-while
如果一直都是默认policy时系统选择deferred
int main() {
    // auto future = std::async(print);  // (概念上)异步执行f
    auto future = std::async(std::launch::deferred, print);  // 模拟默认默认policy时系统选择deferred的情况

std::future_status status;

        do
        {
            status = future.wait_for(std::chrono::seconds(2));
            if (status == std::future_status::deferred) // 延时调用,如果线程还未执行。就直接跳出循环。直接使用get 启动线程,并阻塞完成
            {
                // 先判断系统是否选择policy是deferred,防止死循环
                // 可以直接break,后直接调get
                std::cout << "deferred\n";
                break;
            }
            else if (status == std::future_status::timeout) // 超时调用
            {
                std::cout << "timeout\n"; //  如果是timeout 也就是线程还在运行,就再等待2s ,直到线程read
            }
            else if (status == std::future_status::ready)
            {
                std::cout << "ready!\n";
            }
        } while (status != std::future_status::ready);// timeout 就进入while

        // std::cout << future.get(); // 如果不调用get且在因为 deferred而break出while循环的时候,线程将不会执行。
        std::cout << now() << " all finsh!" << std::endl;
    }

输出结果

main thread1
deferred
1674826194 print start!1
1674826201 print end!
9991674826201 all finsh!
如果如果一直都是默认policy时系统选择deferred, 并且不加 get()
  • 运行结果
main thread1
deferred
1674826241 all finsh!
如果一直都是默认policy时系统选择async
  • 运行结果
main thread1
1674826532 print start!2
timeout
timeout
timeout
1674826539 print end!
ready!
999
1674826539 all finsh!

存在意义

在已经有了td::future、std::promise和std::packaged_task的情况下,实现异步或多线程间通信,可能觉得已经足够了,真的还要一个std::async来凑热闹吗,std::async表示很委屈:我不是来凑热闹的,我是来帮忙的。是的,std::async是为了 让用户的少费点脑子的,它让这三个对象默契的工作。大概的工作过程是这样的:std::async先将异步操作用std::packaged_task包装起来,然后将异步操作的结果放到std::promise中,这个过程就是创造未来的过程。外面再通过future.get/wait来获取这个未来的结果,怎么样,std::async真的是来帮忙的吧,你不用再想到底该怎么用std::future、std::promise和 std::packaged_task了,std::async已经帮你搞定一切了!这就是我们前面说的,std::async类似封装了threadpackged_task的功能。使得我们使用起来更加方便简单。

总结:

std::async可以理解为是更高层次上的异步操作,使我们不用关注线程创建内部细节,就能方便的获取异步执行状态和结果,还可以指定线程创建策略,它是对线程更高层次的抽象,可以优先选用std::async,但它是不能完全代替thread的,比如下面这个就无法用std::async代替:

std::thread([]
{
    // do other things
}).detach();
复制代码

另外,对于std::async,要优先使用std::async的默认策略,除非不满足上述使用条件,这会给予标准库更大的线程管理弹性。除非确实需要异步,才指定std::launch::async策略。

如果std::async满足不了使用需求,则使用std::thread,如:

  • 需要访问底层线程实现的API,如pthread库,设置线程优先级和亲和性。std::thread提供了native_handle成员函数
  • 需要且能够为应用优化线程用法,如执行时的性能剖析情况已知,且作为唯一的主要进程部署在一种硬件特性固定的平台上
  • 需要实现超越cpp并发API的线程技术,如在cpp实现中未提供的线程池的平台上实现线程池

参考

std::async - cppreference.com

(原创)用C++11的std::async代替线程的创建 - 南哥的天下 - 博客园 (cnblogs.com)

《Effective Modern C++》

  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

丁金金

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值