std::async 与 std::thread 在什么时候使用比较合适

前提:

C++ 11 中提供了多线程的标准库,提供了管理线程、保护共享数据、线程间同步操作、原子操作等类。多线程库对应的头文件是 #include <thread>,类名为 std::thread。

然而线程毕竟是比较贴近系统的东西,使用起来仍然不是很方便,特别是线程同步及获取线程运行结果上就更加麻烦。我们不能简单的通过 thread.join() 得到结果,必须定义一个线程共享的变量来传递结果,同时还要考虑线程间的互斥问题。好在 C++ 中提供了一个相对简单的异步接口 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: 延迟加载方式创建线程。调用 aysnc 时不创建线程,直到调用了 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 的基本用法:

#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"

如果需要深入了解 std::async,可以参阅:std::async的使用总结

需要注意的地方,

当 std::async 使用默认参数启动时,它是这两种策略的组合,本质上使行为不可预测。使用带有默认启动参数的 std:async 还存在一系列其他复杂情况(包括无法预测线程局部变量是否被正确访问,异步任务存在根本无法运行的风险),因为 .get( ) 或 .wait() 可能不会在所有等待未来状态准备就绪的代码路径和循环中被调用,因为 std::async 返回的未来可能以延迟状态开始。

因此,为避免所有这些复杂情况,始终使用 std::launch::async 启动参数启动 std::async。

错误的使用方法,

//run myFunction using default std::async policy                          
auto myFuture = std::async(myFunction);

正确的使用方法,

//run myFunction asynchronously                          
auto myFuture = std::async(std::launch::async, myFunction);

那么什么时候使用 std::async 或者 std::thread 呢?

当我们使用  std::async (使用异步启动策略)时,我们是在说:

“我想在单独的线程上完成这项工作”。

当我们使用  std::thread 时,我们是在说:

“我想在一个新线程上完成这项工作”。

细微的差别意味着 std::async 通常使用线程池实现。这意味着如果我们多次调用一个方法 std::async,该方法中的线程 ID 通常会重复,即 std::async 从池中将多个作业分配给同一组线程。然而 std::thread,它永远不会。

这种差异意味着 std::thread 可能比 std::async 更占资源。

当然,这不意味着 std::async 更具优势,

目前,std::async 对于相当简单的程序,它可能最适合处理非常长时间运行的计算或长时间运行的 IO,它不太适合更细粒度的工作负载。为此,使用 std::thread 或使用 Microsoft 的 PPL 或 Intel 的 TBB 之类的东西来滚动自己的线程池。

参考:

posted @ 2022-05-28 14:38  strive-sun  阅读(505)  评论(0编辑  收藏  举报