乐趣区

关于c++:stdasync的使用总结

C++98规范中并没有线程库的存在,直到 C++11 中才终于提供了多线程的规范库,提供了治理线程、爱护共享数据、线程间同步操作、原子操作等类。多线程库对应的头文件是#include <thread>,类名为std::thread

然而线程毕竟是比拟贴近零碎的货色,应用起来依然不是很不便,特地是线程同步及获取线程运行后果上就更加麻烦。咱们不能简略的通过 thread.join() 失去后果,必须定义一个线程共享的变量来传递后果,同时还要思考线程间的互斥问题。好在 C++11 中提供了一个绝对简略的异步接口 std::async,通过这个接口能够简略的创立线程并通过std::future 中获取后果。以往都是本人去封装线程实现本人的 async,当初有线程的跨平台接口能够应用就极大的不便了 C ++ 多线程编程。

先看一下 std::async 的函数原型

//(C++11 起) (C++17 前)
template< class Function, class... Args>
std::future<std::result_of_t<std::decay_t<Function>(std::decay_t<Args>...)>>
    async(Function&& f, Args&&... args);

//(C++11 起) (C++17 前)
template< class Function, class... Args >
std::future<std::result_of_t<std::decay_t<Function>(std::decay_t<Args>...)>>
    async(std::launch policy, Function&& f, Args&&... args);  

第一个参数是线程的创立策略,有两种策略可供选择

  • std::launch::async:在调用 async 就开始创立线程。
  • std::launch::deferred:提早加载形式创立线程。调用 async 时不创立线程,直到调用了 future 的 get 或者 wait 时才创立线程。

默认策略是:std::launch::async | std::launch::deferred也就是两种策略的合集,具体什么意思前面具体再说

第二个参数是线程函数

线程函数可承受function, lambda expression, bind expression, or another function object

第三个参数是线程函数的参数

不再阐明

返回值 std::future

std::future是一个模板类,它提供了一种拜访异步操作后果的机制。从字面意思上看它示意将来,这个意思就十分贴切,因为她不是立刻获取后果然而能够在某个时候以同步的形式来获取后果。咱们能够通过查问 future 的状态来获取异步操作的构造。future_status 有三种状态:

  • deferred:异步操作还未开始
  • ready:异步操作曾经实现
  • timeout:异步操作超时,次要用于 std::future<T>.wait_for()

示例:

// 查问 future 的状态
std::future_status status;
do {status = future.wait_for(std::chrono::seconds(1));
    if (status == std::future_status::deferred) {std::cout << "deferred" << std::endl;} else if (status == std::future_status::timeout) {std::cout << "timeout" << std::endl;} else if (status == std::future_status::ready) {std::cout << "ready!" << std::endl;}
} while (status != std::future_status::ready); 

std::future获取后果的形式有三种:

  • get:期待异步操作完结并返回后果
  • wait:期待异步操作完结,但没有返回值
  • waite_for:超时期待返回后果,下面示例中就是对超时期待的应用展现

介绍完了 std::async 的函数原型,那么它到底该如何应用呢?

std::async的根本用法:示例链接

#include <iostream>
#include <vector>
#include <algorithm>
#include <numeric>
#include <future>
#include <string>
#include <mutex>

std::mutex m;
struct X {void foo(int i, const std::string& str) {std::lock_guard<std::mutex> lk(m);
        std::cout << str << '' << i <<'\n';
    }
    void bar(const std::string& str) {std::lock_guard<std::mutex> lk(m);
        std::cout << str << '\n';
    }
    int operator()(int i) {std::lock_guard<std::mutex> lk(m);
        std::cout << i << '\n';
        return i + 10;
    }};

template <typename RandomIt>int parallel_sum(RandomIt beg, RandomIt end){
    auto len = end - beg;
    if (len < 1000)
        return std::accumulate(beg, end, 0);

    RandomIt mid = beg + len/2;
    auto handle = std::async(std::launch::async,
                             parallel_sum<RandomIt>, mid, end);
    int sum = parallel_sum(beg, mid);
    return sum + handle.get();}

int main(){std::vector<int> v(10000, 1);
    std::cout << "The sum is" << parallel_sum(v.begin(), v.end()) << '\n';

    X x;
    // 以默认策略调用 x.foo(42, "Hello"):// 可能同时打印 "Hello 42" 或提早执行
    auto a1 = std::async(&X::foo, &x, 42, "Hello");
    // 以 deferred 策略调用 x.bar("world!")
    // 调用 a2.get() 或 a2.wait() 时打印 "world!"
    auto a2 = std::async(std::launch::deferred, &X::bar, x, "world!");
    // 以 async 策略调用 X()(43):// 同时打印 "43"
    auto a3 = std::async(std::launch::async, X(), 43);
    a2.wait();                     // 打印 "world!"
    std::cout << a3.get() << '\n'; // 打印 "53"} // 若 a1 在此点未实现,则 a1 的析构函数在此打印 "Hello 42"

可能的后果

The sum is 10000
43
world!
53
Hello 42

由此可见,std::async是异步操作做了一个很好的封装,使咱们不必关注线程创立外部细节,就能不便的获取异步执行状态和后果,还能够指定线程创立策略。

深刻了解线程创立策略

  • std::launch::async 调度策略意味着函数必须异步执行,即在另一线程执行。
  • std::launch::deferred 调度策略意味着函数可能只会在 std::async 返回的 future 对象调用 get 或 wait 时执行。那就是,执行会推延到其中一个调用产生。当调用 get 或 wait 时,函数会同步执行,即调用者会阻塞直到函数运行完结。如果 get 或 wait 没有被调用,函数就相对不会执行。

两者策略都很明确,然而该函数的默认策略却很乏味,它不是你显示指定的,也就是第一个函数原型中所用的策略即std::launch::async | std::launch::deferred,c++ 规范中给出的阐明是:

进行异步执行还是惰性求值取决于实现

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。这意味着上面的循环,看起来最终会进行,然而,实际上可能会始终运行:

void func()           // f 睡眠 1 秒后返回
{std::this_thread::sleep_for(1);
}
auto future = std::async(func);      //(概念上)异步执行 f
while(fut.wait_for(100ms) !=         // 循环直到 f 执行完结
      std::future_status::ready)     // 但这可能永远不会产生
{...}

为防止陷入死循环,咱们必须查看 future 是否把工作推延,然而 future 无奈获知工作是否被推延,一个好的技巧就是通过 wait_for(0)来获取 future_status 是否是 deferred:

auto future = std::async(func);      //(概念上)异步执行 f
if (fut.wait_for(0) == std::future_status::deferred)  // 如果工作被推延
{...     // fut 应用 get 或 wait 来同步调用 f} else {            // 工作没有被推延
    while(fut.wait_for(100ms) != std::future_status::ready) { // 不可能有限循环
      ...    // 工作没有被推延也没有就绪,所以做一些并发的事件直到工作就绪
    }
    ...        // fut 就绪
}

有人可能会说既然有这么多毛病为啥还要用它,因为毕竟咱们思考的极限状况下的可能,有时候我不要求它是并发还是同步执行,也不须要思考批改那个线程 thread_local 变量,同时也能承受可能工作永远不会执行,那么这种形式就是一种不便且高效的调度策略。

综上所述,咱们总结出以下几点:

  • std::async 的默认调度策略既容许工作异步执行,又容许工作同步执行。
  • 默认策略灵活性导致了应用 thread_local 变量时的不确定性,它隐含着工作可能不会执行,它还影响了基于超时的 wait 调用的程序逻辑。
  • 如果异步执行是必须的,指定 std::launch::async 发射策略。

参考文章:

API Reference Document

用 C ++11 的 std::async 代替线程的 创立

退出移动版