深入探讨 C++11 中引入的四个核心异步编程工具:std::async, std::future, std::promise, 和 std::packaged_task。它们共同构成了 C++ 现代并发编程的基础。

为了更好地理解,我们可以使用一个餐厅点餐的类比:

  • std::future (取餐凭证):这是你点餐后拿到的“小票”或“取餐号”。你拿着它,可以稍后去查询(wait)你的餐是否做好了,或者直接等待并取餐(get)。这个凭证本身不能做餐,只能用来获取最终的结果。
  • std::async (套餐服务):这是最省心的方式,就像点一个“全自动套餐”。你告诉柜台你要什么(调用函数),系统(C++运行时)会自动安排一位厨师(一个新线程)去做,并直接给你一个取餐凭证(future)。你什么都不用管,等着取就行了。
  • std::promise (后厨的承诺):这更像是后厨内部的沟通机制。假设一个厨师(生产者线程)向另一个服务员(消费者线程)承诺“我一定会把这道菜做出来”。厨师持有“承诺书”(promise),可以在菜做好后把结果放进去。服务员则持有与这份承诺书配对的“取餐凭gingzheng”(future)。这种方式将“承诺做”和“等待取”这两个动作在不同线程中分离开来。
  • std::packaged_task (打包好的任务单):这就像一张标准化的“任务卡片”,上面写明了要做什么菜(要执行的函数)以及附带了一张可撕下的取餐凭证(future)。你可以把这张任务卡片创建好,但先不交给任何厨师。之后,你可以把它交给任何一个有空的厨师(任何一个线程)去执行。这非常适合任务队列和线程池的场景。

在深入探讨之前,我们先执行一次搜索,以确保所有信息的准确性和时效性。


核心概念与关系

这四个工具都定义在 <future> 头文件中,它们的核心目标是实现线程间的同步和数据传递。

  • std::future 是统一的“结果接收端”。 [1][2]
  • std::async, std::promise, std::packaged_task 都可以看作是“结果的生产者”,它们都能创建一个与之关联的 std::future 对象,但创建和使用方式各不相同。 [3][4]

下面我们逐一详细解析。

1. std::future:未来的凭证

std::future 是一个对象,它代表了一个异步操作的最终结果。你可以把它想象成一个占位符,这个“坑”未来会被某个值或者一个异常填满。

主要操作:

  • get(): 等待异步操作完成,然后获取其结果。这个函数会阻塞当前线程直到结果可用。注意:get() 只能被调用一次
  • wait(): 阻塞当前线程,直到结果可用,但不获取结果。
  • wait_for(), wait_until(): 带超时的等待。
  • valid(): 检查 future 是否与某个共享状态相关联(即是否有效)。

std::future 本身不启动任何线程或任务,它纯粹是用来接收结果的。

2. std::async:最高级的异步任务启动器

std::async 是一个函数模板,它的作用是启动一个异步任务,并返回一个持有该任务结果的 std::future[5] 这是最简单、最直接的异步编程方式。

特点:

  • 高度封装: 你只需要提供一个可调用对象(如函数、lambda)及其参数,std::async 会负责线程的创建和管理。
  • 启动策略 (Launch Policy): 这是 std::async 的一个关键特性,可以通过第一个参数指定: [6][7]
    • std::launch::async: 强制在一个新线程中立即异步执行任务。
    • std::launch::deferred: 延迟执行。任务不会立即开始,而是在你对返回的 future 调用 get()wait() 时,才在调用者的线程中同步执行。
    • 默认(不指定或使用 std::launch::async | std::launch::deferred): 由C++运行时库根据系统负载等情况自行决定是创建新线程还是延迟执行。
  • 析构函数行为: 由 std::async 返回的 std::future 对象,如果在其任务完成前被销毁,其析构函数会阻塞直到任务执行完毕。 [3] 这是一个重要的陷阱,必须确保在 future 销毁前,其结果已经被获取或等待。

用法示例:

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

int long_computation(int input) {
    std::cout << "Thinking..." << std::endl;
    std::this_thread::sleep_for(std::chrono::seconds(2));
    return input * 10;
}

int main() {
    // 启动一个异步任务
    std::future<int> result_future = std::async(std::launch::async, long_computation, 5);

    // 在主线程中做其他事情
    std::cout << "Main thread is doing other work." << std::endl;

    // 当需要结果时,调用get()
    // 这会阻塞,直到 long_computation 完成
    int result = result_future.get();

    std::cout << "The result is: " << result << std::endl;

    return 0;
}

3. std::promise:一个明确的承诺

std::promise 提供了一种在线程间手动设置值或异常的机制。 [8][9] 它和 std::future 是一对一的“推-拉”关系:promise 负责“推”入一个值,而 future 负责“拉”取这个值。 [1][10]

特点:

  • 解耦: promise 将“设置值”的动作和“获取值”的动作完全分离开来。你可以在一个线程中创建 promisefuture,然后将 promise 移动到另一个线程去设置值。
  • 显式控制: 你可以精确控制何时、何地设置值(set_value)或异常(set_exception)。
  • 一次性使用: 每个 promise 只能设置一次值或异常。 [9]

用法示例:

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

void worker_thread(std::promise<std::string> p) {
    try {
        // 模拟一些工作
        std::this_thread::sleep_for(std::chrono::seconds(2));
        // 工作完成,兑现承诺
        p.set_value("Data from worker thread");
    } catch (...) {
        // 如果发生异常,设置异常
        p.set_exception(std::current_exception());
    }
}

int main() {
    // 创建一个 promise
    std::promise<std::string> data_promise;

    // 从 promise 获取 future
    std::future<std::string> data_future = data_promise.get_future();

    // 启动工作线程,并将 promise 移交给它
    // promise 不能被拷贝,只能移动
    std::thread t(worker_thread, std::move(data_promise));

    // 主线程做其他事情
    std::cout << "Main thread waiting for data..." << std::endl;

    // 等待并获取结果
    std::string data = data_future.get();
    std::cout << "Received data: " << data << std::endl;

    t.join();
    return 0;
}

4. std::packaged_task:打包好的待执行任务

std::packaged_task 是一个类模板,它包装一个可调用对象(函数、lambda等),并允许其结果被异步地获取。 [11][12] 它像是一个将“任务”和“获取结果的凭证”捆绑在一起的包裹。

特点:

  • 任务与执行分离: packaged_task 将任务的定义和任务的执行分离开来。你可以先创建一个 packaged_task,在稍后的某个时间点,再把它交给一个线程去执行。
  • 线程池的基石: 这个特性使得 packaged_task 成为实现线程池等任务队列系统的理想工具。 [13][14] 你可以创建一个 packaged_task 队列,然后让工作线程从中取出任务并执行。
  • 自带 Future: 创建 packaged_task 后,可以立即通过 get_future() 方法获取与之关联的 future 对象。 [12]

用法示例:

#include <iostream>
#include <future>
#include <thread>
#include <functional>
#include <vector>
#include <queue>

int calculate_sum(int a, int b) {
    return a + b;
}

int main() {
    // 1. 打包一个任务
    std::packaged_task<int(int, int)> task(calculate_sum);

    // 2. 获取与任务关联的 future
    std::future<int> result_future = task.get_future();

    // 3. 将任务移动到线程中执行
    // packaged_task 也不能被拷贝,只能移动
    std::thread t(std::move(task), 10, 20);

    // 主线程等待结果
    int result = result_future.get();
    std::cout << "The sum is: " << result << std::endl;

    t.join();
    return 0;
}

总结与对比

特性 std::async std::promise std::packaged_task
抽象级别
核心作用 启动一个异步任务并返回 future 在线程间手动传递一个值或异常 包装一个可调用对象,将其与 future 绑定
线程管理 自动 (由运行时库决定) 手动 (需要自己创建和管理线程) 手动 (需要自己将任务对象传递给线程执行)
耦合度 任务的调用和执行紧密耦合 值的“生产者”和“消费者”完全解耦 任务的“定义”和“执行”解耦
主要用例 简单的“即发即忘”式异步调用 复杂的线程间通信,事件驱动模型 任务队列,线程池实现

何时使用哪个?

  • 优先选择 std::async: 如果你的需求仅仅是“在后台运行这个函数,我稍后需要它的结果”,那么 std::async 是最简单、最安全、最推荐的选择。它能避免很多手动管理线程的麻烦。

  • 当需要精细控制时,使用 std::promise: 如果你的结果不是由一个简单的函数调用产生的,而是由一系列复杂的事件或计算决定的,或者当设置值的线程和获取值的线程生命周期完全独立时,std::promise 提供了所需的灵活性。

  • 构建任务系统时,使用 std::packaged_task: 如果你需要创建一个任务队列,让一组工作线程去处理,或者需要将任务的创建和执行分离开来(例如,在主线程中创建任务,在工作线程中执行),std::packaged_task 是最合适的构建块。它是实现线程池的完美工具。 [13][15]


Learn more:

  1. cpp-notes/future-and-promise.md at master - GitHub
  2. 【C++并发编程】std::future、std::async、std::packaged_task与std::promise的深度探索(一)-阿里云开发者社区
  3. C++ 中async、packaged_task、promise 区别及使用原创 - CSDN博客
  4. C++ 并发操作的同步 - GuKaifeng’s Blog
  5. C++ async | how the async function is used in C++ with example? - EDUCBA
  6. async、packaged_task、promise、future的区别与使用 - L_B__
  7. std::async
  8. std::promise in C++ - GeeksforGeeks
  9. std::promise - cppreference.com
  10. Concurrency in C++ : Passing Data between Threads — Promise-Future - Medium
  11. C++ – 一文搞懂std::future、std::promise、std::packaged_task、std::async的使用和相互区别-StubbornHuang Blog
  12. Packaged Task | Advanced C++ (Multithreading & Multiprocessing) - GeeksforGeeks
  13. Making a Thread Pool in C++ from scratch - DEV Community
  14. Building a Thread Pool with C++ and STL - Coding Notes
  15. Getting Started With C++ Thread-Pool | by Bhushan Rane | Medium

补充:C++20引入了一些新的异步编程工具

a. 协程 (Coroutines)

这是最具革命性的变化,它引入了一种全新的异步编程模型。协程可以看作是可以暂停和恢复的函数。

核心理念:与 std::async 启动一个可能阻塞的线程不同,协程可以在等待一个操作(如网络I/O)时非阻塞地挂起自身,让出执行权。当操作完成后,它可以从挂起的位置恢复执行。这使得单个线程能够高效管理成千上万的并发任务。
新关键字:引入了 co_await, co_yield, co_return 三个关键字来定义和控制协程的行为。
优势:能够以近似同步的方式编写逻辑清晰的异步代码,彻底告别“回调地狱”(Callback Hell)。 它非常适合I/O密集型应用,如高性能网络服务器。
状态:C++20 提供了协程的底层语言支持,但上层的高级封装仍在发展中,通常需要配合像 Boost.Asio 这样的库来发挥最大威力。

b. std::jthread

std::jthread 是对 std::thread 的一个安全、现代的替代品。 [8]

自动 join: jthread 的析构函数会自动调用 join(),这意味着你不再需要手动管理线程的生命周期,从而避免了忘记 join() 或 detach() 导致的程序终止问题。这是它相比 std::thread 最显著的优势。
协作式中断: jthread 内置了停止令牌 (stop token) 机制 (std::stop_source, std::stop_token)。你可以从外部请求一个 jthread 停止,而线程内部可以通过检查 stop_token 的状态来优雅地退出循环,实现了协作式的任务取消。

c. 同步原语:std::latch 和 std::barrier

这两个工具用于协调多个线程的执行时机。

std::latch (门闩): 这是一个一次性的同步点。 你可以初始化一个计数器,多个线程到达后使计数器减一,当计数器减到零时,所有在 wait() 处等待的线程被同时唤醒。它非常适合“等待所有工作线程准备就绪后,一起开始执行任务”的场景。
std::barrier (屏障): 与 latch 类似,但它是可重用的。当所有线程都到达屏障点后,它们被释放,然后屏障会自动重置,可用于下一轮同步。这非常适合迭代式算法,其中每一步计算都需要所有线程同步。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐