Effective Modern C++ 条款36:如果有异步的必要请指定std::launch::async
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::async调用
启动策略
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_localint tlsVar =0;voidf(){ tlsVar =42;// 哪个线程的tlsVar被修改?}auto fut = std::async(f);// 危险!无法确定tlsVar属于哪个线程2. 基于超时的等待循环可能无限执行
考虑以下看似合理的代码:
usingnamespace std::literals;voiddelayedTask(){ std::this_thread::sleep_for(1s);}auto fut = std::async(delayedTask);// 看似最多等待10次,实际可能无限循环!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模板函数
为了简化使用,我们可以创建一个包装函数:
// C++14版本template<typenameF,typename... Ts>autoreallyAsync(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;});五、应用案例:并行图像处理
考虑一个图像处理应用,我们需要异步执行多个滤镜操作:
structImage{/*...*/}; Image applySepia(Image img){/*...*/} Image applyBlur(Image img){/*...*/}voidprocessImage(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
考虑默认策略
确保真正并行
接受不确定性
通过理解并正确应用std::async的启动策略,我们能够在C++并发编程中既保持灵活性,又不失确定性,编写出既高效又可靠的多线程代码。