上一篇我们剖析了 future/promise/packaged_task 的底层机制。std::async 是建立在它们之上的更高层抽象——一行代码即可创建异步任务并获取 future。但 std::async 也是 C++ 标准库中最容易被误解和误用的组件之一:它的启动策略行为微妙、返回 future 的析构有隐式 join 语义、与线程池的性能差异巨大。本文将剖析 std::async 的两种启动策略、底层实现、异常传播机制,以及在实际工程中的适用边界。
一、问题引入:std::async 的"魔法"与陷阱
1.1 看似简单的异步
#include <future>
int compute(int x) {
// 耗时计算
return x * 2;
}
int main() {
// 一行代码:异步执行 compute,获取 future
std::future<int> f = std::async(std::launch::async, compute, 21);
// 做其他工作…
int result = f.get(); // 等待并获取结果
// result == 42
}
std::async 看起来非常优雅——不需要手动创建线程、不需要手动管理 promise、不需要手动 join。它自动完成了:
但这种"魔法"背后隐藏着许多微妙的行为和陷阱。
1.2 最著名的陷阱:隐式 join
// ❌ 看似异步,实际同步
std::async(std::launch::async, []() {
long_running_task();
}); // 返回的 future 立即析构 → 隐式 join → 阻塞!
// 这行代码要等 long_running_task 完成后才会执行
do_other_work();
std::async 返回的 future 在析构时,如果任务是用 launch::async 启动的,会隐式地 join 线程——阻塞直到任务完成。这意味着如果不保存返回的 future,std::async 的行为和同步调用没有区别!
这是 C++ 标准中一个有争议的设计决策,也是很多并发 bug 的根源。
二、API 速览:std::async
2.1 基本用法
#include <future>
// 形式1:指定启动策略
std::future<int> f1 = std::async(std::launch::async, compute, 21);
std::future<int> f2 = std::async(std::launch::deferred, compute, 21);
std::future<int> f3 = std::async(std::launch::async | std::launch::deferred, compute, 21);
// 形式2:不指定启动策略(等价于 async | deferred,由实现选择)
std::future<int> f4 = std::async(compute, 21);
2.2 启动策略
| std::launch::async | 立即创建新线程执行任务(异步) |
| std::launch::deferred | 延迟执行,直到 future 的 get() 或 wait() 被调用时才在当前线程执行(同步) |
| async | deferred | 由实现选择(默认行为) |
2.3 不指定策略的默认行为
如果不指定启动策略(或指定 async | deferred),标准允许实现自行选择:
- MSVC:总是选择 async(创建新线程)
- libstdc++(GCC):总是选择 async(创建新线程)
- libc++(Clang):总是选择 async(创建新线程)
虽然标准允许选择 deferred,但实际上所有主流实现都选择 async。因此不指定策略时,std::async 几乎总是创建新线程。
但这不是标准保证的——可移植代码不应该依赖这个行为。
三、底层原理:std::async 的实现
3.1 std::async 的内部流程
std::async 本质上是对 std::thread + std::packaged_task 的封装:
// std::async 的简化实现
template<typename F, typename... Args>
future<result_of_t<F(Args...)>> async(launch policy, F&& f, Args&&... args) {
using ResultType = result_of_t<F(Args...)>;
// 1. 创建 packaged_task
packaged_task<ResultType()> task(
std::bind(std::forward<F>(f), std::forward<Args>(args)...)
);
// 2. 获取 future
future<ResultType> result = task.get_future();
// 3. 根据启动策略执行
if (policy == launch::async) {
// 创建新线程执行任务
thread t(std::move(task));
t.detach(); // 分离线程,由 future 的析构来 join
} else if (policy == launch::deferred) {
// 延迟执行:将任务存储在 future 的 shared_state 中
// 等到 get() 或 wait() 时才在当前线程执行
result._M_set_deferred_task(std::move(task));
}
return result;
}
关键设计点:
3.2 async 策略:future 析构的隐式 join
这是 std::async 最关键、最容易被误解的行为。当用 launch::async 启动任务时,返回的 future 的 shared_state 中存储了线程的信息。future 析构时:
~future() {
if (valid()) {
// 如果是 async 启动的任务,且线程还在运行
if (state->_M_is_async_task && state->_M_thread_running) {
state->_M_thread.join(); // 隐式 join!阻塞等待线程完成
}
state->_M_remove_reference();
}
}
标准规定(C++14 及以后明确):
如果 future 是由 std::async(launch::async, …) 返回的,且 shared_state 还未就绪,future 的析构函数会阻塞直到任务完成。
这意味着:
{
auto f = std::async(launch::async, long_task);
// f 析构时,如果 long_task 还没完成,会阻塞等待
} // 隐式 join
为什么这样设计?
如果 future 析构时不 join,detach 的线程可能继续访问已经销毁的局部变量(通过引用捕获),导致未定义行为。标准选择"隐式 join"来避免这种悬空引用问题。
但这个设计导致了一个反直觉的结果:不保存 future 的 std::async 调用是同步的。
3.3 deferred 策略:延迟执行
launch::deferred 策略下,任务不立即执行,而是存储在 shared_state 中。当调用 get() 或 wait() 时,任务在调用线程中同步执行:
auto f = std::async(launch::deferred, []() {
std::cout << "任务执行在线程: " << std::this_thread::get_id() << std::endl;
return 42;
});
// 此时任务还没执行
std::cout << "调用 get 前" << std::endl;
int result = f.get(); // 在当前线程执行任务!
// 输出:任务执行在线程: [当前线程ID]
deferred 策略的特点:
auto f = std::async(launch::deferred, long_task);
// wait_for 不会触发执行,立即返回 deferred
auto status = f.wait_for(std::chrono::seconds(1));
// status == future_status::deferred
// 只有 get() 或 wait() 才会触发执行
f.wait(); // 现在执行任务
3.4 两种策略的行为对比
| 是否创建新线程 | ✅ 是 | ❌ 否 |
| 何时执行 | 立即(在新线程) | 延迟(get/wait 时在当前线程) |
| future 析构是否阻塞 | ✅ 是(隐式 join) | ❌ 否 |
| wait_for 返回 | ready/timeout | deferred(未执行时) |
| 并发度 | 高(真正异步) | 无(同步执行) |
| 适用场景 | 真正需要异步执行 | 惰性求值、可能不需要结果 |
四、异常传播
4.1 std::async 的异常处理
std::async 内部使用 packaged_task,因此异常会自动被捕获并通过 future 传播:
auto f = std::async(launch::async, []() -> int {
throw std::runtime_error("异步任务失败");
});
try {
int result = f.get(); // 重新抛出异常
} catch (const std::runtime_error& e) {
std::cout << "捕获异常: " << e.what() << std::endl;
}
4.2 析构时的异常处理
一个微妙的问题:如果 future 析构时隐式 join,而任务抛出了异常,会发生什么?
{
auto f = std::async(launch::async, []() {
throw std::runtime_error("任务失败");
});
// f 析构时隐式 join,任务的异常被存储在 shared_state 中
// 但因为没有调用 get(),异常不会被重新抛出
// 异常被"吞掉"了!
}
如果不调用 get(),任务中的异常会被存储在 shared_state 中,然后随 shared_state 一起被销毁——异常被静默吞掉。
这是一个潜在的问题:异步任务可能失败了,但你永远不知道。
最佳实践:始终在 future 上调用 get()(或至少 wait() 后检查状态),确保异常被正确处理。
五、源码剖析:libstdc++ 的实现
5.1 std::async 的核心代码
libstdc++ 中 std::async 定义在 <future> 中:
template<typename _Fn, typename... _Args>
future<__async_result_of<_Fn, _Args...>>
async(launch __policy, _Fn&& __fn, _Args&&... __args) {
using __result_type = __async_result_of<_Fn, _Args...>;
// 创建 packaged_task
__shared_state_base::__state_type __state =
__make_shared_state<__result_type>();
auto __task = __make_packaged_task<__result_type()>(
__state, std::bind(std::forward<_Fn>(__fn),
std::forward<_Args>(__args)...));
if ((__policy & launch::async) != launch{}) {
// async 策略:创建新线程
__try {
thread __t(std::move(__task));
__state->_M_thread_ = __t.native_handle();
__t.detach();
__state->_M_policy = launch::async;
} __catch(...) {
__state->_M_set_exception(std::current_exception());
}
} else {
// deferred 策略:存储任务
__state->_M_deferred_task = std::move(__task);
__state->_M_policy = launch::deferred;
}
return future<__result_type>(__state);
}
5.2 future 析构的隐式 join
~future() {
if (_M_state) {
// 如果是 async 启动的任务
if (_M_state->_M_policy == launch::async) {
// 等待线程完成(隐式 join)
_M_state->_M_wait();
}
_M_state->_M_remove_reference();
}
}
注意:libstdc++ 的实现中,future 析构时调用 _M_wait() 等待任务完成,这就是隐式 join。
5.3 deferred 任务的执行
void _State_base::_M_wait() {
if (_M_policy == launch::deferred && _M_deferred_task) {
// deferred 任务:在当前线程执行
_M_deferred_task();
_M_deferred_task = nullptr;
}
// 正常等待(条件变量)
std::unique_lock<std::mutex> __lock(_M_mutex);
_M_cond.wait(__lock, [this]{ return _M_ready; });
}
deferred 任务在 _M_wait() 中被执行——在调用 get() 或 wait() 的线程中同步执行。
六、工程实践
6.1 始终保存返回的 future
铁律:使用 std::async 时,始终保存返回的 future,否则任务会同步执行。
// ❌ 错误:future 立即析构,隐式 join,任务同步执行
std::async(launch::async, background_task);
do_other_work(); // 要等 background_task 完成后才执行
// ✅ 正确:保存 future
auto f = std::async(launch::async, background_task);
do_other_work(); // 与 background_task 并发执行
f.get(); // 或 f.wait()
6.2 用 std::async 还是线程池?
| 少量长时间任务 | std::async | 简单,不需要管理线程池 |
| 大量短任务 | 线程池 | 避免频繁创建线程的开销 |
| 需要控制并发度 | 线程池 | std::async 每次创建新线程,可能创建过多 |
| 一次性异步操作 | std::async | 简洁方便 |
| 持续的任务调度 | 线程池 | 线程池支持任务队列和调度策略 |
std::async 的性能问题:
每次调用 std::async(launch::async, …) 都会创建新线程。如果频繁调用(如每秒数百次),线程创建开销和上下文切换会成为瓶颈。此外,没有并发度限制——如果同时调用 1000 次 std::async,会创建 1000 个线程,可能导致系统资源耗尽。
经验法则:
- 任务执行时间 > 10ms,且并发数量少 → std::async
- 任务执行时间 < 1ms,或并发数量大 → 线程池
6.3 deferred 策略的巧妙用法
deferred 策略虽然不常用,但在某些场景下很有用:
场景1:可能不需要结果的惰性计算
auto f = std::async(launch::deferred, []() {
return expensive_computation();
});
if (need_result()) {
auto result = f.get(); // 只有需要时才执行
use_result(result);
}
// 如果不需要结果,任务永远不执行,零开销
场景2:确保任务在特定线程执行
// deferred 任务在调用 get() 的线程执行
// 可以用来确保任务在主线程执行(如 UI 更新)
auto f = std::async(launch::deferred, ui_update_task);
// … 在主线程 …
f.get(); // ui_update_task 在主线程执行
6.4 常见误用
误用1:在类成员函数中用 std::async 捕获 this
class MyClass {
public:
void start() {
future_ = std::async(launch::async, &MyClass::worker, this);
}
~MyClass() {
// future_ 析构时隐式 join,等待 worker 完成
// 但如果 worker 访问 this,而 this 正在析构…
}
private:
void worker() {
// 访问成员变量
data_ = 42;
}
std::future<void> future_;
int data_;
};
这个模式有潜在问题:析构函数中 future_ 析构会隐式 join,等待 worker 完成。但 worker 可能正在访问正在析构的对象。虽然 join 能确保 worker 在析构完成前结束,但成员变量的析构顺序可能导致问题。
更安全的做法是在析构函数中显式处理:
~MyClass() {
stop_flag_.store(true); // 请求停止
if (future_.valid()) {
future_.get(); // 显式等待
}
}
误用2:用 std::async 执行永不返回的任务
auto f = std::async(launch::async, []() {
while (true) {
// 无限循环
}
});
// f 析构时会永远阻塞!
如果任务永不返回,future 析构时的隐式 join 会永远阻塞。对于长期运行的任务,应该用 std::thread + detach,或用停止标志配合显式 join。
误用3:假设不指定策略一定创建线程
auto f = std::async(task); // 不指定策略
// 标准允许实现选择 deferred!
// 虽然主流实现都选择 async,但可移植代码不应依赖
如果需要确保异步执行,显式指定 launch::async。
6.5 std::async 与 std::thread 的对比
| 创建线程 | 是(async 策略) | 是 |
| 获取返回值 | ✅(通过 future) | ❌(需要输出参数) |
| 异常传播 | ✅(通过 future) | ❌(未捕获异常导致 terminate) |
| 自动 join | ✅(future 析构隐式 join) | ❌(需要手动 join/detach) |
| 延迟执行 | ✅(deferred 策略) | ❌ |
| 并发度控制 | ❌(每次创建新线程) | 手动控制 |
| 适用场景 | 一次性异步任务 | 长期运行的线程 |
七、性能考量
7.1 std::async 的开销
| std::async(launch::async) 创建 | ~20-60μs(线程创建 + shared_state 分配) |
| future::get(已就绪) | ~50ns |
| future::get(阻塞) | 取决于任务执行时间 |
| future 析构(隐式 join) | 取决于任务剩余时间 |
| std::async(launch::deferred) 创建 | ~50-100ns(仅 shared_state 分配) |
| deferred 任务执行(get 时) | 任务本身的执行时间 |
async 策略的主要开销是线程创建(20-60μs),和直接用 std::thread 一样。deferred 策略几乎没有创建开销(只分配 shared_state)。
7.2 与线程池的性能对比
| 任务提交开销 | 20-60μs | <1μs |
| 最大并发度 | 无限制(可能创建过多线程) | 可配置 |
| 内存占用 | 每个线程 1-8MB 栈 | 固定数量线程 |
| 适用任务时长 | >10ms | 任意(尤其短任务) |
对于大量短任务,线程池的性能可能比 std::async 高 10-100 倍。
八、面试高频问题
Q1:std::async 的两种启动策略有什么区别?
launch::async 立即创建新线程异步执行任务;launch::deferred 延迟执行,直到 future 的 get() 或 wait() 被调用时才在当前线程同步执行。关键区别:(1)async 创建新线程,deferred 不创建;(2)async 立即执行,deferred 惰性执行;(3)async 返回的 future 析构时会隐式 join(阻塞等待线程完成),deferred 的 future 析构不阻塞;(4)deferred 任务未执行时,wait_for/wait_until 立即返回 future_status::deferred。不指定策略时(或 async|deferred),标准允许实现选择,主流实现都选择 async。
Q2:什么是 std::async 的隐式 join?它导致了什么著名陷阱?
用 launch::async 启动任务时,返回的 future 的 shared_state 中存储了线程信息。future 析构时,如果任务还未完成,会阻塞等待线程完成(隐式 join)。这是标准规定的行为(C++14明确),目的是避免 detach 线程访问已销毁的局部变量。著名陷阱:如果不保存返回的 future,future 立即析构,隐式 join 导致任务同步执行——std::async 变成了同步调用!例如 std::async(launch::async, task); do_work(); 中,do_work() 要等 task 完成后才执行。因此使用 std::async 必须始终保存返回的 future。
Q3:std::async 和 std::thread 有什么区别?各自适用场景?
区别:(1)std::async 返回 future,可以获取返回值和异常;std::thread 不支持返回值,异常未捕获会导致 terminate;(2)std::async 的 future 析构隐式 join,std::thread 必须手动 join 或 detach;(3)std::async 支持 deferred 延迟执行,std::thread 总是立即执行;(4)std::async 不控制并发度(每次创建新线程),std::thread 手动管理。适用场景:std::async 适合一次性异步任务、需要获取返回值的场景;std::thread 适合长期运行的线程、需要精细控制线程生命周期的场景。大量短任务两者都不适合,应该用线程池。
Q4:std::async 的异常是如何传播的?如果不调用 get() 会怎样?
std::async 内部使用 packaged_task 封装任务,任务执行时抛出的异常会被自动捕获并存储在 shared_state 中。调用 future::get() 时,如果结果是异常,会调用 std::rethrow_exception 重新抛出。如果不调用 get()(也不调用 wait()),异常会一直存储在 shared_state 中,随 shared_state 一起被销毁——异常被静默吞掉,调用者永远不知道任务失败了。因此最佳实践是始终在 future 上调用 get(),确保异常被正确处理。
Q5:std::async 为什么不适合大量短任务?线程池好在哪里?
std::async(launch::async) 每次调用都创建新线程,线程创建开销约 20-60μs,且每个线程占用 1-8MB 栈空间。对于大量短任务(执行时间 <1ms),线程创建开销占比过高,且大量线程的上下文切换和内存占用会成为瓶颈。此外 std::async 没有并发度限制,同时调用 1000 次会创建 1000 个线程,可能耗尽系统资源。线程池通过复用已创建的线程,将任务提交开销降到 <1μs,并可配置最大并发度,避免资源耗尽。经验法则:任务执行时间 >10ms 且并发少用 std::async;任务短或并发大用线程池。
Q6:launch::deferred 策略有什么实际用途?
deferred 策略下任务不立即执行,而是存储在 shared_state 中,等到 get() 或 wait() 时在调用线程同步执行。实际用途:(1)惰性求值——可能不需要结果的昂贵计算,只有真正需要时才执行,不需要则零开销;(2)确保任务在特定线程执行——deferred 任务在调用 get() 的线程执行,可以用来确保任务在主线程执行(如 UI 更新);(3)零开销的 future 封装——需要 future 接口但不需要真正异步时,deferred 避免了线程创建开销。注意 deferred 任务未执行时 wait_for 立即返回 deferred 状态,不会触发执行。
下一篇预告:第 10 篇《std::call_once 与 once_flag:线程安全的延迟初始化》。我们将剖析 once_flag 的内部状态机、call_once 的双重检查锁定(DCLP)实现、Meyers Singleton(函数局部静态变量)的线程安全保证、异常安全与重试机制,以及 C++11 前后单例模式的演变。




