C++11 引入了一个用于执行异步任务的标准库函数 std::async,同时引入的还有 std::future 和 std::promise。
std::async 自动处理了线程管理和任务调度的细节,使得异步执行任务变得很简单。
std::future 和 std::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::promise 和 std::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::promise 和 std::future 都不支持拷贝构造,一种方法是定义 多个 std::promise 和 std::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 - 简单使用线程,非循环处理任务的,任务执行完成之后自动退出,不需要获取任务执行结果:
async、jthread、thread - 线程循环处理任务,需要外部线程停止线程:
jthread - 线程池:
thread+stop_token+stop_source组合使用
需要注意的点:
-
std::promise和std::future都不支持拷贝构造,传递时要用引用或者右值引用。 - 需要确保
std::future仅调用get()一次,否则会抛出std::future_error异常。 - 需要把同一消息传递给多个线程,用
std::shared_future。