C++扩展 --- 并发支持库(补充2)https://blog.csdn.net/Small_entreprene/article/details/149854780?spm=1001.2014.3001.5501

在多线程编程中,我们经常需要从线程中获取任务执行的结果。比如启动一个线程压缩文件后,我们需要知道压缩后的文件名和大小。在 C++11 之前,获取线程返回值并不直观,通常需要借助指针、互斥锁和条件变量的组合,代码复杂且容易出错。

传统方式的痛点

先看看没有 future 库时,我们是如何获取线程返回值的:

#include<iostream>
#include<thread>
#include<mutex>

void fun(int x, int y, int* ans) {
    *ans = x + y;
}

int main()
{
    int a = 10;
    int b = 8;

    int* sum = new int(0);
    std::thread t(fun, a, b, sum);
    t.join();

    // 获取线程的"返回值"
    std::cout << *sum << std::endl; // 输出:18
    delete sum;
    
    return 0;
}

这段代码虽然能工作,但存在明显问题:

  • 需要手动管理动态内存(new/delete
  • 必须显式调用 join() 等待线程完成
  • 无法优雅地处理多个返回值
  • 没有异常处理机制

如果线程需要在不同阶段返回多个结果,代码会变得更加复杂,需要更多的同步机制来协调线程间的数据传递。

什么是 future 库?

C++11 引入的 <future> 头文件彻底改变了传统线程获取返回值的复杂方式,它提供了一套现代化的异步编程机制。其中 std::future 是一个类模板(class template),专门用于接收异步任务的返回值或异常,是 C++ 异步编程的核心组件。

简单来说,std::future 就像是一张类型安全的提货单:当我们通过 std::asyncstd::packaged_taskstd::promise 启动一个异步任务时,系统会立刻返回一个与任务返回值类型匹配的 future<T> 对象;

我们不需要立即等待任务结束,而是可以在后续需要结果时,调用 future.get() 来获取返回值。如果任务尚未完成,get() 会阻塞等待;如果任务已完成,则直接返回结果;若任务抛出异常,get() 也会将异常抛出,让主线程可以安全捕获,实现了异步操作、结果获取与异常处理的一体化,比手动管理线程、共享内存和同步机制更简洁、更安全。【核心要点】

std::future 只能调用一次 get(),第一次调用后共享状态就会被释放,再次 get() 会导致未定义行为,想让多个线程获取结果要用 std::shared_future。【注意】【所以我们应该按需保存】

future 的核心组件

<future>库主要包含以下几个核心组件:

  1. std::future:获取异步操作结果的 "提货单"
  2. std::async:启动异步任务的便捷函数
  3. std::promise:用于主动设置异步操作结果
  4. std::packaged_task:封装可调用对象为异步任务

下面我们逐一了解这些组件的用法。

1. std::async:最简单的异步任务

std::async是启动异步任务最简便的方式,它会自动创建线程并运行任务,返回一个std::future对象供我们获取结果。

#include <iostream>
#include <thread>
#include <future>
#include <chrono>

int add(int x, int y) {
    std::cout << "[add] : my thread id is " << std::this_thread::get_id() << std::endl;
    std::this_thread::sleep_for(std::chrono::seconds(1)); // 模拟业务处理时间
    return x + y; 
}

int main() {
    std::cout << "[main] : my thread id is " << std::this_thread::get_id() << std::endl;
    std::cout << "========== 使用 async 开启一个处理任务, 返回 future 封装好的结果 ===========" << std::endl;
    int x, y;
    std::cout << "请输入要计算的数:" << std::endl;
    std::cout << "x = "; std::cin >> x;
    std::cout << "y = "; std::cin >> y;
    std::future<int> result = std::async(add, x, y);
    std::cout << "等待结果中..." << std::endl;
    std::this_thread::sleep_for(std::chrono::seconds(1));
    std::cout << "计算结果: " << result.get() << std::endl;
    return 0;
}
lfz@U22:~/WorkSpace/myLearn/future$ ./main
[main] : my thread id is 127777395471296
========== 使用 async 开启一个处理任务, 返回 future 封装好的结果 ===========
请输入要计算的数:
x = 100
y = 230
等待结果中...
[add] : my thread id is 127777388885568
计算结果: 330

这段代码比传统方式简洁得多,我们不需要手动创建线程、管理内存,也不需要显式调用 join()

所以 std::async 是 C++11 用来启动一个异步任务的函数,你给它传一个函数 / 可调用对象,它就会在后台自动运行不用你自己创建 thread、不用 join、不用管线程生命周期

它会直接返回一个 std::future,你后面用 future.get() 就能拿到返回值。

那为什么不需要主线程进行join呢?

因为 std::async 会返回一个 std::future 对象,而等待线程结束、获取返回值的逻辑已经被封装在 future.get() 内部,当我们调用 get() 时,如果【异步任务】所在线程还没有执行完毕,get() 会自动阻塞当前主线程,一直等待线程运行完成,这个行为本质上和 std::threadjoin() 是一样的,相当于系统在底层帮我们自动完成了等待线程结束的操作,不需要我们再手动调用 join()而如果不调用 get(),只要 future 对象正常生命周期结束,相关的线程资源也会被合理管理,不会像直接使用 std::thread 忘记 join() 那样触发程序异常,所以使用 std::async 时,主线程不再需要显式写 join()

启动策略

std::async提供了两种启动策略:

  • std::launch::async:立即创建新线程执行任务
  • std::launch::deferred:延迟执行,直到调用 get () 时才在当前线程执行
#include <iostream>
#include <thread>
#include <future>
#include <chrono>
#include <sstream>
#include <string>

std::string showInfo(const std::string& name) {
    // 获取线程 ID
    std::thread::id tid = std::this_thread::get_id();

    // 转成 string
    std::ostringstream oss;
    oss << tid;
    std::string tid_str = oss.str();

    std::string idInfo = "[" + name + "] 线程 ID: " + tid_str;

    return idInfo;
}


int main() {
    std::cout << "========== 使用 async 的不同策略开启一个处理任务 ===========" << std::endl;
    std::future task1 = std::async(std::launch::async, showInfo, "异步任务");
    std::string deferred_msg = "延迟任务";
    std::future task2 = std::async(std::launch::deferred, showInfo, std::ref(deferred_msg));
    std::cout << "[main] : 线程 ID: " << std::this_thread::get_id() << std::endl;

    std::cout << "等待结果中..." << std::endl;
    std::this_thread::sleep_for(std::chrono::seconds(1));

    std::cout << task1.get() << std::endl;
    std::cout << task2.get() << std::endl;

    return 0;
}
========== 使用 async 的不同策略开启一个处理任务 ===========
[main] : 线程 ID: 124725928223680
等待结果中...
[异步任务] 线程 ID: 124725921576512
[延迟任务] 线程 ID: 124725928223680

std::async 存在两种关键启动策略,会直接影响任务执行方式:std::launch::async 表示立即创建新线程并在后台异步执行任务,属于真正的异步;

std::launch::deferred 则不会创建线程,只会延迟执行,直到调用 get()wait() 时才在当前线程同步运行函数,并非异步

而默认不指定策略时,等效于同时使用两种策略,由系统自行选择,行为不确定,是容易踩坑的地方。

同时,无论采用哪种启动策略,我们都不需要手动调用 join(),因为使用 std::launch::async 时,get() 内部会自动阻塞等待线程结束,相当于完成了 join 的工作,使用 std::launch::deferred 时则直接在当前线程执行,根本不存在线程需要等待的问题,因此 std::async 结合 std::future 彻底省去了手动管理线程和 join 的操作。

2. std::promise:主动设置结果

在 C++11 引入的并发编程模型中,std::promisestd::future 是一对相辅相成的工具,它们共同构建了异步操作中 **“结果传递”的桥梁。简单来说,std::promise 负责 “生产” 结果,而 std::future 负责 “消费” 结果,二者通过一个隐藏的 “共享状态” 实现跨线程通信。

具体来说:

std::promise 和 std::future 本质上是一对一的生产者 - 消费者模型,一个 promise 只能对应一个 future,不支持一对多、多对多,所以不存在我们担心的 “多个生产覆盖数据”“多个线程重复消费” 的问题,因为标准库设计上就限制了只能写一次、只能读一次。它的底层共享状态需要保证:生产者写入后,消费者能被精准唤醒,并且等待时不占用 CPU,所以实现上不会用轻量的原子 + CAS,而是使用互斥锁保护共享状态,条件变量实现等待与唤醒,这样既能保证线程安全,又能让消费者在没有结果时进入休眠,避免空轮询,是最稳定、最符合 C++ 标准要求的实现方式。

虽然 std::promisestd::future一对一、只写一次、只读一次,从逻辑上避免了多写、重复消费,但它们之间的共享状态不是原子操作,且 future 的阻塞等待必须靠条件变量实现,而条件变量本身强制依赖互斥锁才能安全等待与唤醒,所以底层依然必须用锁,锁是为了保证共享状态读写完整、等待唤醒机制可靠,不是为了防并发覆盖。

先看一个基础示例,感受它们的协作方式:

#include <iostream>
#include <thread>
#include <future>

int main() {
    std::cout << "========== promise 与 future 生产消费关系 ===========" << std::endl;
    std::promise<int> producer;
    // 绑定生产方
    std::future<int> consumer = producer.get_future();

    std::thread t([&producer](){
        // 模拟耗时操作(如网络请求、文件IO等)
        std::this_thread::sleep_for(std::chrono::seconds(1));
        // 任务完成后,通过promise设置结果,触发共享状态就绪[就是生产产品,通知消费]
        producer.set_value(42);  
    });

    // 主线程中,future 等待结果就绪并获取
    std::cout << "等待异步结果..." << std::endl;
    // get()会阻塞直到结果可用,且只能调用一次[因为绑定方只生产了一个产品]
    std::cout << "获取到的结果: " << consumer.get() << std::endl;
    // std::cout << "获取到的结果: " << consumer.get() << std::endl; // 再次就会报错

    t.join();
    
    return 0;
}
lfz@U22:~/WorkSpace/myLearn/future$ ./main
========== promise 与 future 生产消费关系 ===========
等待异步结果...
获取到的结果: 42

std::promise的独特价值

std::promise最强大的特性在于其 "主动性"—— 它允许我们在任意时机、任意线程中设置结果,而非局限于异步任务本身。这种灵活性使其适用于多种场景:

  • 分阶段任务:例如一个线程负责预处理数据,另一个线程在预处理完成后通过promise设置中间结果,主线程则通过future获取并继续处理。
  • 异常传递:当异步操作发生错误时,可通过promise.set_exception()将异常存储到共享状态,future.get()会在主线程中重新抛出该异常,实现跨线程异常安全传递。
  • 外部事件触发:比如等待用户输入、信号量触发等外部事件,完成后通过promise手动标记结果就绪。

std::future的核心区别

虽然二者总是成对出现,但职责边界清晰:

维度 std::promise std::future
角色 结果的 "生产者" 结果的 "消费者"
核心操作 set_value()/set_exception()(设置结果) get()/wait()(获取 / 等待结果)
状态影响 主动将共享状态置为 "就绪" 被动等待共享状态变为 "就绪"
生命周期 通常与生产者线程绑定 通常与消费者线程绑定
复制性 不可复制,仅可移动(确保结果唯一设置) 不可复制,仅可移动(结果只能被获取一次)

需要注意的是,future.get()是一次性操作 —— 一旦调用,共享状态的结果会被 "取走",再次调用将导致未定义行为。如果需要在多个线程中获取同一结果,可使用std::shared_future(通过future.share()转换),它支持多次获取结果。

其实到这里,我们就可以这么去理解:

std::promise + std::future 本质上就是封装好的、绑定一对一的条件变量 + 互斥锁 + 共享状态。

  • future.get() 没数据时,就是 condition_variable.wait(),阻塞休眠、不占 CPU;
  • promise.set_value() 就是把值写进共享状态,然后自动调用 notify_one(),精准唤醒绑定的那个 future;
  • 它和你手写 mutex + condition_variable 的底层逻辑一模一样,只是 C++ 把 “等待、唤醒、共享状态、锁” 全部打包好了,你不用自己写,更安全、更少出错。

一句话:future = 封装好的 waitpromise = 封装好的 notify_one它们就是语法糖版、安全版、一对一版的条件变量

扩展:与其他异步工具的配合

std::promisestd::future是 C++ 异步编程的基础,它们还能与更高层次的工具配合使用:

  • std::packaged_task:将函数或可调用对象包装为一个 "任务",自动创建关联的future,本质是对promise的封装(任务执行完毕后自动调用set_value)。
  • std::async:更简洁的异步接口,可直接启动异步任务并返回future,无需手动管理线程和promise,底层可能使用线程池优化性能。

例如,用std::async简化上述示例:

// 自动管理线程,返回的future直接关联任务结果
std::future<int> fut = std::async(std::launch::async, []() {
    std::this_thread::sleep_for(std::chrono::seconds(1));
    return 42;
});
std::cout << "结果: " << fut.get() << std::endl;

综上,std::promisestd::future构成了 C++ 异步编程的 "结果通道",前者赋予我们主动控制结果的能力,后者则提供了安全获取结果的机制。理解这对工具的协作模式,是掌握 C++ 并发编程的重要一步。

3. std::packaged_task:封装任务

std::packaged_task 是 C++11 引入的异步编程工具,它能将可调用对象(函数、lambda、函数对象等)包装起来,自动创建关联的 std::future,当任务执行完毕后,结果会自动存储到共享状态中,供 future 获取。

也就是说:std::packaged_task 就是封装了 promise 的任务包装器,它内部自动创建 promise、自动关联 future,我们只需要给它一个函数,它执行完函数后自动把返回值塞给 promise,我们通过 future.get() 拿结果就行。

基本语法结构

// 声明:包装返回类型为T,参数类型为Args...的可调用对象
std::packaged_task<T(Args...)> task(可调用对象);

// 获取关联的future
std::future<T> fut = task.get_future();

// 执行任务(两种方式)
task(args...);                  // 直接在当前线程执行
std::thread t(std::move(task), args...);  // 转移到新线程执行
#include <iostream>
#include <future>
#include <cmath>
#include <thread>

// 计算平方根的函数
double compute_square_root(double x) {
    return std::sqrt(x);
}

int main() {
    // 封装任务
    std::packaged_task<double(double)> task(compute_square_root);
    // 获取future对象
    std::future<double> result = task.get_future();
    
    // 在新线程中执行任务
    std::thread th(std::move(task), 25.0);
    
    // 获取结果
    std::cout << "25的平方根是: " << result.get() << std::endl;
    
    th.join();
    return 0;
}
  • 创建 packaged_task:把函数 compute_square_root 包进去,内部自动生成一个 promise
  • 获取 futuretask.get_future() 拿到和内部 promise 绑定的 future。
  • 把任务扔到线程执行:线程启动后运行函数,计算出结果。
  • 自动设置结果函数返回值自动被 packaged_task 调用 promise.set_value(返回值)
  • 主线程 get ():阻塞等待 → 拿到结果。
  • join 等待线程结束

std::packaged_task非常适合那些需要重复执行的任务,我们可以像传递普通函数一样传递它。

future 的常用方法

std::future提供了一系列方法来管理和获取异步操作的结果:

  • get():获取结果,如果任务未完成则阻塞等待
  • wait():等待任务完成,但不获取结果
  • wait_for(duration):等待指定时长,返回等待状态
  • wait_until(timepoint):等待到指定时间点,返回等待状态
  • valid():检查 future 是否有效(是否关联到一个异步任务)
#include <iostream>
#include <future>
#include <chrono>

int long_task() {
    std::this_thread::sleep_for(std::chrono::seconds(3));
    return 42;
}

int main() {
    auto fut = std::async(long_task);
    
    // 等待最多1秒
    auto status = fut.wait_for(std::chrono::seconds(1));
    
    if (status == std::future_status::ready) {
        std::cout << "任务已完成,结果: " << fut.get() << std::endl;
    } else if (status == std::future_status::timeout) {
        std::cout << "等待超时,任务仍在执行..." << std::endl;
    } else if (status == std::future_status::deferred) {
        std::cout << "任务被延迟执行" << std::endl;
    }
    
    // 最终还是要获取结果
    std::cout << "最终结果: " << fut.get() << std::endl;
    
    return 0;
}

异常处理

异步任务中抛出的异常会被 future 捕获,当调用get()时会重新抛出,让我们可以在主线程中统一处理异常:

#include <iostream>
#include <future>
#include <stdexcept>

void risky_operation() {
    // 模拟一个可能失败的操作
    throw std::runtime_error("操作失败: 资源不足");
}

int main() {
    std::future<void> fut = std::async(risky_operation);
    
    try {
        fut.get();  // 可能会抛出异常
    } catch (const std::exception& e) {
        // 在主线程中处理异常
        std::cout << "捕获到异常: " << e.what() << std::endl;
    }
    
    return 0;
}

这种机制确保了异步任务的异常不会被忽略,并且可以按照我们熟悉的方式处理。

总结

<future>库为 C++ 异步编程提供了强大而优雅的解决方案:

  1. 简化了线程返回值的获取方式,无需手动管理同步机制
  2. 提供了多种异步任务的创建方式(async、promise、packaged_task)
  3. 内置了灵活的等待机制和异常处理
  4. 让代码更加清晰、简洁、易于维护

从传统的线程 + 指针 + 锁的复杂组合,到使用 future 库的简洁代码,C++ 的异步编程体验得到了质的飞跃。掌握 future 库,能让你在多线程编程中更加得心应手,编写出更高质量的并发代码。

在实际开发中,我们可以根据具体需求选择合适的组件:简单异步任务用std::async,需要主动设置结果用std::promise,封装可重用任务用std::packaged_task

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐