std::async 和 std::future的使用

核心场景 1:并行执行多个独立任务("Fire and Forget" + 结果聚合)

最常见的场景是:你有 N 个互不干扰的任务(比如读取 10 个文件、进行 3 次不同的网络请求),让它们并行跑,最后汇总结果。

代码示例:并行数据处理

假设我们要对一个大数组的不同部分进行并行计算。

cpp 复制代码
#include <iostream>
#include <vector>
#include <future>
#include <numeric> // 用于 std::accumulate
#include <chrono>

// 定义一个计算部分和的函数
int sum_part(const std::vector<int>& data, size_t start, size_t end) {
    // 模拟耗时计算
    std::this_thread::sleep_for(std::chrono::milliseconds(100));
    return std::accumulate(data.begin() + start, data.begin() + end, 0);
}

int main() {
    // 1. 准备数据(1亿个整数,仅作演示)
    std::vector<int> big_data(10000, 1); 

    // 2. 确定并行策略(例如:根据硬件并发数分块)
    unsigned int num_threads = std::thread::hardware_concurrency();
    if (num_threads == 0) num_threads = 4; // 保底值
    
    size_t block_size = big_data.size() / num_threads;
    
    std::vector<std::future<int>> futures; // 存储所有 future

    std::cout << "启动 " << num_threads << " 个异步任务..." << std::endl;

    // 3. 启动多线程任务
    for (unsigned int i = 0; i < num_threads; ++i) {
        size_t start = i * block_size;
        size_t end = (i == num_threads - 1) ? big_data.size() : (i + 1) * block_size;
        
        // 使用 std::launch::async 强制开启新线程
        futures.push_back(std::async(std::launch::async, sum_part, std::cref(big_data), start, end));
    }

    // 4. 主线程可以在这里做其他事...
    std::cout << "主线程继续忙碌,等待子线程计算..." << std::endl;

    // 5. 汇总结果(这是一个典型的 MapReduce 模式)
    int total = 0;
    for (auto& fut : futures) {
        total += fut.get(); // 如果某个线程没跑完,这里会阻塞等待
    }

    std::cout << "计算完成,总和: " << total << std::endl;
    return 0;
}

关键点解析:

  1. 容器管理 Futures :我们用 std::vector<std::future<int>> 来持有所有的 future 对象。
  2. std::cref :因为数据量大,我们传递 const 引用以避免拷贝,必须用 std::cref 包装。
  3. std::launch::async :在多线程场景下,通常建议显式指定这个策略 ,确保任务真的在并行跑,而不是延迟到 get() 时才在主线程跑。

核心场景 2:流水线与依赖链(Future 的链式调用)

有时候,任务 B 依赖任务 A 的结果,任务 C 依赖任务 B 的结果。我们可以利用 .get() 的阻塞特性来构建依赖链。

代码示例:串行依赖的并行
cpp 复制代码
#include <iostream>
#include <future>
#include <string>

// 步骤 1: 加载数据
std::string load_data() {
    std::cout << "[Thread A] 正在加载数据..." << std::endl;
    std::this_thread::sleep_for(std::chrono::seconds(1));
    return "RawData_12345";
}

// 步骤 2: 解析数据(依赖步骤1的结果)
std::string parse_data(std::string raw) {
    std::cout << "[Thread B] 正在解析: " << raw << std::endl;
    std::this_thread::sleep_for(std::chrono::seconds(1));
    return "Parsed{" + raw + "}";
}

// 步骤 3: 保存数据(依赖步骤2的结果)
void save_data(std::string parsed) {
    std::cout << "[Thread C] 正在保存: " << parsed << std::endl;
    std::this_thread::sleep_for(std::chrono::seconds(1));
}

int main() {
    // 启动 A
    std::future<std::string> f1 = std::async(std::launch::async, load_data);
    
    // 启动 B,传入 f1.get()。注意:这里 main 线程会阻塞,直到 f1 完成,然后才会启动 f2
    // 如果想让 A 和 B 完全解耦并行,那它们之间不应该有直接的数据依赖
    std::future<std::string> f2 = std::async(std::launch::async, parse_data, f1.get());
    
    // 启动 C
    std::future<void> f3 = std::async(std::launch::async, save_data, f2.get());

    f3.get(); // 等待最后一步完成
    std::cout << "所有流水线步骤完成!" << std::endl;
    
    return 0;
}

核心场景 3:避免数据竞争(用 Future 替代共享变量)

在多线程中,最头疼的是数据竞争(Data Race) 。使用 std::future 的一大优势是:它通过"返回值"传递结果,而不是通过修改共享内存。

反面教材(不要这样做):
cpp 复制代码
// 危险!没有锁保护的共享变量
int shared_result = 0;

void unsafe_increment() {
    for(int i=0; i<10000; ++i) shared_result++; // 竞态条件
}
正面教材(使用 Future):
cpp 复制代码
#include <iostream>
#include <future>

int safe_calculation() {
    int res = 0;
    for(int i=0; i<10000; ++i) res++;
    return res; // 只操作局部变量,线程安全
}

int main() {
    auto f1 = std::async(std::launch::async, safe_calculation);
    auto f2 = std::async(std::launch::async, safe_calculation);
    
    // 最后在主线程汇总,完全不需要 mutex
    int total = f1.get() + f2.get(); 
    std::cout << "Total: " << total << std::endl;
    return 0;
}

多线程环境下的"深坑"与注意事项

1. 小心"隐形阻塞"(Future 的析构函数)

这是 C++ 标准中最容易踩坑的地方。请看下面的代码:

cpp 复制代码
void bad_code() {
    // 启动了一个异步任务,但是没有把返回的 future 赋给变量!
    std::async(std::launch::async, [](){
        std::this_thread::sleep_for(std::chrono::seconds(5));
        std::cout << "Task done!" << std::endl;
    });
    
    // 注意:上面的 std::async 返回了一个临时 future 对象。
    // 这行代码结束后,临时 future 被析构。
    // 由于策略是 async,析构函数会***阻塞在这里等待 5 秒***,直到线程跑完!
    std::cout << "This line is blocked!" << std::endl;
}

解决办法 :只要你用了 std::launch::async,就一定要把返回的 future 保存到变量中,哪怕你不需要它的返回值。

2. std::future 不能被拷贝,只能移动

std::futureMove-only 的。你不能把它放进容器里通过拷贝赋值,必须用 std::move

cpp 复制代码
std::future<int> f = std::async(func);
std::vector<std::future<int>> vec;
// vec.push_back(f); // 编译错误!不能拷贝
vec.push_back(std::move(f)); // 正确,使用移动语义
3. 异常的跨线程传播

如果在 std::async 启动的函数里抛出了异常,程序不会直接崩溃。异常会被存储在 future 里,直到你调用 .get() 的时候,异常会在调用 .get() 的那个线程(通常是主线程)被重新抛出。

这是好事 ,这意味着你可以在主线程用一个 try-catch 块捕获所有子线程的异常。


总结:最佳实践清单

  1. 优先使用 std::async 而非 std::thread :除非你需要极其底层的线程控制,否则 async 更安全、更方便(自动管理线程池、处理返回值)。
  2. 显式指定 std::launch::async:除非你确定想要延迟求值(Deferred),否则为了保证真正的并行,显式指定策略。
  3. 保存 Future 对象:避免临时对象析构导致的意外阻塞。
  4. 通过返回值通信 :尽量利用 future 的返回值传递数据,减少对 std::mutex 的依赖。
相关推荐
墨韵流芳2 小时前
CCF-CSP第41次认证第一题——平衡数
c++·算法·ccf·平衡数
水饺编程2 小时前
第4章,[标签 Win32] :SysMets3 程序讲解01
c语言·c++·windows·visual studio
郝学胜-神的一滴2 小时前
巧解括号序列分解问题:栈思想的轻量实现
开发语言·数据结构·c++·算法·面试
代码改善世界3 小时前
【C++初阶】string类(一):从基础到实战
开发语言·c++
计算机安禾3 小时前
【数据结构与算法】第15篇:队列(二):链式队列的实现与应用
c语言·开发语言·数据结构·c++·学习·算法·visual studio
迷途之人不知返3 小时前
初次学习模板
c++
程序猿追3 小时前
HarmonyOS 6.0 实战:用 Native C++ NDK 开发一款本地计步器应用
c++·华为·harmonyos
Q741_1473 小时前
每日一题 力扣 2840. 判断通过操作能否让字符串相等 II 力扣 2839. 判断通过操作能否让字符串相等 I 找规律 字符串 C++ 题解
c++·算法·leetcode·力扣·数组·找规律
kyle~3 小时前
ROS2 ---- TF2坐标变换(1.动态、静态发布,2.缓存,3.监听)
c++·机器人·ros2