C++并发编程之 async

C++11 引入了一个用于执行异步任务的标准库函数 std::async,同时引入的还有 std::futurestd::promise
std::async 自动处理了线程管理和任务调度的细节,使得异步执行任务变得很简单。
std::futurestd::promise 则是用来实现异步任务间消息通信的。

1 async 介绍

gcc 中 libstdc++ 库里,std::sync有两个重载版本的模板函数,分别为:

template <typename _Fn, typename... _Args>
future<__async_result_of<_Fn, _Args...>> async(_Fn &&__fn, _Args &&...__args);

template <typename _Fn, typename... _Args>
future<__async_result_of<_Fn, _Args...>> async(launch __policy, _Fn &&__fn, _Args &&...__args);

async 执行策略
std::async 虽然是异步执行任务,但不一定会启动新线程来处理任务的,具体的执行策略(std::launch)有三种:

  • std::launch::async: 立即启动一个新线程来执行任务。
  • std::launch::deferred:不会在调用时立即执行函数,而是等到第一次调用 get() 方法时才执行该函数。这种情况下,函数将在当前线程中执行,而不是在新线程中执行。
  • std::launch::async | std::launch::deferred:如果当前有可用的线程,std::async 将会在一个新线程中执行;如果没有可用线程,则会在当前线程中延迟执行。

不带 std::launch 参数的 std::async 函数使用的执行策略是 std::launch::async | std::launch::deferred

std::async 是否启动线程取决于你选择的执行策略。如果你希望避免不必要的线程创建,并且可以接受在当前线程中延迟执行任务,那么可以选择 std::launch::deferred。如果你需要真正的异步执行,并且不在乎创建新线程的成本,那么可以选择 std::launch::async

为了把讨论限定在并发编程,下面把执行策略仅仅限定在 std::launch::async来讨论,即启动新线程来执行任务。

2 async 的使用

以前我们如果要在线程间传递数据,就要用互斥量和条件变量来同步,接口又多,处理共享数据又要小心翼翼避免死锁,写出来正确的多线程共享数据还是有门槛的。
std::async 的返回值是一个 std::future 对象,通过 std::future 对象可以获取任务的结果,可以看出来子线程传递数据(任务执行结果)给主线程是是容易的。

2.1 子线程传递消息给主线程

一个简单的计算连乘任务异步执行的示例代码如下:

#include <future>
#include <iostream>

int factorial(int num)
{
    int res = 1;
    for(int i = 1; i <= num; ++i)
    {
        res *= i;
    }
    return res;
}

int main(int argc, char *argv[])
{
    int num = 5;
    std::future<int> f = std::async(std::launch::async, factorial, num);
    std::cout << "get factorial(" << num << ") result: " << f.get() << std::endl;
    return 0;
}

这也是最常用的模式,子线程传递任务结果给主线程。

2.2 主线程传递消息给子线程

为了更加通用化的传递消息,可以在主线程和子线程间或者不同的子线程之间相互传递,而不是仅仅限定从子线程传递消息给主线程,标准库给了 std::promise 组件,它代表了一个线程对另一个异步任务的承诺,承诺会给你某些消息。
需要注意的是,std::promisestd::future 都不支持拷贝构造,传递时要用引用或者右值引用。
仍然用计算连乘任务异步执行的示例代码演示下:

#include <future>
#include <iostream>

int factorial(std::future<int> &mf)
{
    int num = mf.get();
    int res = 1;
    for(int i = 1; i <= num; ++i)
    {
        res *= i;
    }
    return res;
}

int main(int argc, char *argv[])
{
    int num = 5;
    std::promise<int> p;
    std::future<int> mf = p.get_future();
    std::future<int> f = std::async(std::launch::async, factorial, std::ref(mf));
    p.set_value(num); // 主线程传递消息给子线程
    std::cout << "get factorial(" << num << ") result: " << f.get() << std::endl;
    return 0;
}

演示的是从主线程发消息给子线程,从其他子线程发消息给其他子线程是类似的,不再展示。

2.3 同一个消息传递给多个线程

std::future 只能的消息只能get()一次(重复调用会抛出 std::future_error 异常),这在子线程中传递消息给主线程时,是合理的,开发者只要确保调用 get() 一次就行了。
但是,其他场景中如果需要把相同消息发给多个线程间(一传多),该怎么处理呢?std::promisestd::future 都不支持拷贝构造,一种方法是定义 多个 std::promisestd::future,然后 std::promise 设置相同的值。显然,这种方法效率很低,std::shared_future 就是解决这种问题的。

用相同的数据,执行两次连乘任务:

#include <future>
#include <iostream>

int factorial(std::shared_future<int> &mf)
{
    int num = mf.get();
    int res = 1;
    for(int i = 1; i <= num; ++i)
    {
        res *= i;
    }
    return res;
}

int main(int argc, char *argv[])
{
    int num = 5;
    std::promise<int> p;
    std::shared_future<int> mf(p.get_future());
    std::future<int> f = std::async(std::launch::async, factorial, std::ref(mf));
    std::future<int> f2 = std::async(std::launch::async, factorial, std::ref(mf));
    p.set_value(num); // 主线程传递消息给子线程
    std::cout << "get factorial(" << num << ") result: " << f.get() << std::endl;
    std::cout << "get factorial(" << num << ") result: " << f2.get() << std::endl;
    return 0;
}

3 总结

各个线程组件适用场景:

  • 简单使用线程,非循环处理任务的,任务执行完成之后自动退出,需要获取任务执行结果:async
  • 简单使用线程,非循环处理任务的,任务执行完成之后自动退出,不需要获取任务执行结果:asyncjthreadthread
  • 线程循环处理任务,需要外部线程停止线程:jthread
  • 线程池:thread + stop_token + stop_source 组合使用

需要注意的点:

  • std::promisestd::future 都不支持拷贝构造,传递时要用引用或者右值引用。
  • 需要确保 std::future 仅调用 get() 一次,否则会抛出 std::future_error 异常。
  • 需要把同一消息传递给多个线程,用 std::shared_future
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容