Effective Modern C++ 条款 36:明确指定 std::launch::async 策略
引言:异步编程的艺术
在现代 C++ 并发编程中,std::async 能够协调多个线程执行任务。然而,其默认行为暗藏玄机——并不总是如期望那样立即启动异步任务。本文将探讨 std::async 的启动策略,揭示默认行为的潜在陷阱,并展示如何确保真正的异步执行。
一、std::async 的两种启动策略
std::async 提供了两种基本的启动策略:
std::launch::async - 函数必须异步执行,即在不同的线程上立即开始。
std::launch::deferred - 函数仅在调用 get() 或 wait() 时才开始执行,且在当前线程上同步执行。
| 启动策略 | 执行时机 |
|---|
std::launch::async | 立即在新线程执行 |
std::launch::deferred | 延迟到 get/wait 调用时执行 |
二、默认策略的'双重人格'
令人惊讶的是,std::async 的默认策略并非上述任何一种,而是二者的'或'组合:
auto fut1 = std::async(f);
auto fut2 = std::async(std::launch::async | std::launch::deferred, f);
这种设计赋予了标准库极大的灵活性,使其能够智能管理线程资源、避免线程创建开销并实现负载均衡。然而,这种灵活性也带来了三个关键的不确定性:
- 并发性不确定:函数
f 可能与调用线程并发执行,也可能不会。
- 线程归属不确定:
f 可能在任何线程上执行。
- 执行性不确定:
f 甚至可能永远不会执行。
三、默认策略的潜在陷阱
1. thread_local 变量的不确定性
当函数使用线程局部存储 (thread_local) 时,我们无法预测哪个线程的变量会被访问:
thread_local int tlsVar = 0;
void f() {
tlsVar = 42;
}
auto fut = std::async(f);
2. 基于超时的等待循环可能无限执行
考虑以下看似合理的代码:
using namespace std::literals;
void delayedTask() {
std::this_thread::sleep_for(1s);
}
auto fut = std::async(delayedTask);
while (fut.wait_for(100ms) != std::future_status::ready) {
}
如果任务被延迟执行 (std::launch::deferred),wait_for 将永远返回 std::future_status::deferred,导致无限循环。
四、解决方案:显式指定异步策略
要确保真正的异步执行,必须显式指定 std::launch::async:
auto fut = std::async(std::launch::async, f);
实用工具:reallyAsync 模板函数
为了简化使用,我们可以创建一个包装函数:
template<typename F, typename... Ts>
auto reallyAsync(F&& f, Ts&&... params) {
return std::async(std::launch::async, std::forward<F>(f), std::forward<Ts>(params)...);
}
auto fut = reallyAsync([] {
std::cout << "Running asynchronously!" << std::endl;
});
五、应用案例:并行图像处理
考虑一个图像处理应用,我们需要异步执行多个滤镜操作:
struct Image {};
Image applySepia(Image img) {}
Image applyBlur(Image img) {}
void processImage(const Image& original) {
auto fut1 = reallyAsync(applySepia, original);
auto fut2 = reallyAsync(applyBlur, original);
Image sepia = fut1.get();
Image blurred = fut2.get();
}
| 策略类型 | 执行时机 | 线程使用 | 适用场景 |
|---|
| async | 立即 | 新线程 | 需要真正并行 |
| deferred | get/wait 时 | 调用线程 | 惰性求值 |
| 默认 | 不确定 | 不确定 | 一般情况 |
六、性能考量
虽然 std::launch::async 确保了真正的异步执行,但也需要注意:
- 线程创建开销:每次调用都会创建新线程。
- 系统资源限制:过多的并发任务可能导致资源耗尽。
- 负载均衡:默认策略在这方面更有优势。
在需要严格异步但又要控制资源的情况下,可以考虑使用线程池模式。
七、总结指南
使用 std::async 时,请记住以下准则:
- ✅ 当任务必须异步执行时,显式使用
std::launch::async。
- ✅ 当不确定性和惰性求值可接受时,可以使用默认策略。
- ✅ 对于需要访问
thread_local 或使用超时等待的任务,避免默认策略。
- ✅ 考虑使用
reallyAsync 这样的包装器来简化代码。
决策建议:
- 需要异步执行?是 -> 使用
std::launch::async
- 不确定性和惰性求值可接受?是 -> 考虑默认策略