Effective Modern C++ 条款36:如果有异步的必要请指定std::launch::async

Effective Modern C++ 条款36:如果有异步的必要请指定std::launch::async

引言:异步编程的艺术

在现代C++并发编程中,std::async犹如一位优雅的指挥家,能够协调多个线程和谐地演奏程序交响曲。然而,这位指挥家的默认行为却暗藏玄机------它并不总是如我们所期望的那样立即启动异步任务。本文将深入探讨std::async的启动策略,揭示默认行为的潜在陷阱,并展示如何确保真正的异步执行。

一、std::async的两种启动策略

std::async提供了两种基本的启动策略,如同音乐会的两种演奏方式:

  1. std::launch::async - 如同交响乐团的即时演出,函数必须异步执行,即在不同的线程上立即开始演奏。

  2. std::launch::deferred - 更像是乐谱的延迟解读,函数仅在调用get()wait()时才开始演奏,且在当前线程上同步执行。

std::async调用
启动策略
std::launch::async
std::launch::deferred
立即在新线程执行
延迟到get/wait调用时执行

二、默认策略的"双重人格"

令人惊讶的是,std::async的默认策略并非上述任何一种,而是二者的"或"组合:

cpp 复制代码
auto fut1 = std::async(f);  // 默认策略
auto fut2 = std::async(std::launch::async | std::launch::deferred, f); // 等效写法

这种设计赋予了标准库极大的灵活性,使其能够:

  • 智能管理线程资源
  • 避免线程创建和销毁的开销
  • 实现负载均衡

然而,这种灵活性也带来了三个关键的不确定性:

  1. 并发性不确定 :函数f可能与调用线程并发执行,也可能不会
  2. 线程归属不确定f可能在任何线程上执行
  3. 执行性不确定f甚至可能永远不会执行

三、默认策略的潜在陷阱

1. thread_local变量的不确定性

当函数使用线程局部存储(thread_local)时,我们无法预测哪个线程的变量会被访问:

cpp 复制代码
thread_local int tlsVar = 0;

void f() {
    tlsVar = 42;  // 哪个线程的tlsVar被修改?
}

auto fut = std::async(f);  // 危险!无法确定tlsVar属于哪个线程

2. 基于超时的等待循环可能无限执行

考虑以下看似合理的代码:

cpp 复制代码
using namespace std::literals;

void delayedTask() {
    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

cpp 复制代码
auto fut = std::async(std::launch::async, f);  // 确保异步执行

实用工具:reallyAsync模板函数

为了简化使用,我们可以创建一个包装函数:

cpp 复制代码
// C++14版本
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; 
});

五、应用案例:并行图像处理

考虑一个图像处理应用,我们需要异步执行多个滤镜操作:

cpp 复制代码
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确保了真正的异步执行,但也需要注意:

  1. 线程创建开销:每次调用都会创建新线程
  2. 系统资源限制:过多的并发任务可能导致资源耗尽
  3. 负载均衡:默认策略在这方面更有优势

在需要严格异步但又要控制资源的情况下,可以考虑使用线程池模式。

七、总结指南

使用std::async时,请记住以下准则:

✅ 当任务必须异步执行时,显式使用std::launch::async

✅ 当不确定性和惰性求值可接受时,可以使用默认策略

✅ 对于需要访问thread_local或使用超时等待的任务,避免默认策略

✅ 考虑使用reallyAsync这样的包装器来简化代码


需要异步执行?
使用std::launch::async
考虑默认策略
确保真正并行
接受不确定性

通过理解并正确应用std::async的启动策略,我们能够在C++并发编程中既保持灵活性,又不失确定性,编写出既高效又可靠的多线程代码。

相关推荐
菜菜的顾清寒几秒前
力扣HOT100(49)动态规划 -- 打家劫舍
算法·leetcode·动态规划
葡萄城技术团队4 分钟前
观察生活:人是如何分词的
算法·生活
草莓熊Lotso6 分钟前
【Linux网络】深入理解 HTTP 协议(一):从基础概念到 URL 编码解码
linux·网络·c++·网络协议·http·软件工程
眠りたいです6 分钟前
现代C++:C++17中的新语言特性
开发语言·c++·c++17
一只旭宝8 分钟前
【C++入门精讲17】序列容器
开发语言·c++
Demon1_Coder8 分钟前
Day1-SpringAI-1.0.0版本
java·开发语言·前端
装不满的克莱因瓶10 分钟前
什么是特征分解?从数学定义到现实问题的映射
人工智能·数学·算法·机器学习·ai·特征分解
killerbasd14 分钟前
总结 6.1
算法
郝学胜-神的一滴14 分钟前
Qt 高级开发 021:零基础吃透 QVBoxLayout 垂直布局
开发语言·c++·qt·程序人生·用户界面