c++的`std::future` 与 `std::promise` 简明教程(5分钟读完)

在现代C++编程中,std::future和std::promise是异步编程模型中的两个重要组件,它们构成了C++标准库中处理异步计算结果的基础。本文将深入浅出地介绍这两个概念,探讨它们的应用场景、常见问题、易错点及如何避免,同时辅以代码示例,帮助读者更好地理解和运用这些机制。


1. 它解决什么问题

线程之间要传一个结果,而且这个结果很晚才产生。你不想用全局变量加轮询,也不想为"等结果"手写条件变量。

std::promise / std::future 就是标准库给的答案:一个结果,两个把手。

text 复制代码
        std::promise<int>                 std::future<int>
         (写端)                            (读端)
              |                                 |
              |       共享状态(堆上,引用计数)  |
              +------> [值 42 | 就绪标志] <------+
                       一次性、不可逆
  • 写端 用 set_value() 把结果放进去 ------ 只能放一次。
  • 读端 用 get() 把结果取出来 ------ 只能取一次;没就绪就一直等。

一句话:写端定终局一次,读端取结果一次。 后面所有"只能用一次"的限制,都是这句话推出来的。


2. 最小可用例子

cpp 复制代码
#include <chrono>
#include <future>
#include <iostream>
#include <thread>

int main()
{
    std::promise<int> p;                     // 写端
    std::future<int> f = p.get_future();     // 读端(这一步不阻塞!)

    std::thread t([&p] {
        std::this_thread::sleep_for(std::chrono::milliseconds(50));
        p.set_value(42);                     // 干活干完了,交结果
    });

    std::cout << "waiting for result...\n";
    std::cout << "result = " << f.get() << "\n";   // 阻塞直到就绪,取走结果
    t.join();
}

输出(实测):

text 复制代码
waiting for result...
result = 42

f.get() 之前的 cout 一定先打印 ------ 因为 get() 之前没有任何阻塞。


3. 三条必须记住的规则

规则 说明
get_future() 不等待 它只是取回读端把手,立刻返回
set_value() 只能一次 第二次抛 promise_already_satisfied
get() 通常只能一次 取走后 future 就失效了,再 get() 抛 no_state

第二条和第三条犯错时抛的都是 std::future_error,可以直接抓:

cpp 复制代码
try { p.set_value(2); }                    // 第二次设置
catch (const std::future_error& e) {
    // e.code() == std::future_errc::promise_already_satisfied
}

判断错误类型时请比较枚举值 (e.code() == std::future_errc::no_state), 不要比较 e.code().value() 的数字 ------ 那是实现相关的。


4. 更省事的写法:std::async

不想手动管理 promise 和线程时,让 std::async 替你干:

cpp 复制代码
#include <future>
#include <iostream>

int compute() { return 6 * 7; }

int main()
{
    auto f = std::async(std::launch::async, compute);   // 立刻起线程
    std::cout << "result = " << f.get() << "\n";        // 42
}

但有一个坑必须知道 :std::async 返回的 future,如果没被接住,析构时会阻塞到任务结束。

cpp 复制代码
std::async(std::launch::async, slow_job);   // 返回值被丢弃 → 这行会等 slow_job 跑完

所以这里有个反直觉的结论:

  • std::async 不能用来"扔出去就不管"(即发即忘)------漏接返回值等于同步执行。
  • std::async 也当不了线程池 ------ 每次调用都可能新建线程。

另外,std::async 的默认策略是 async | deferred,实现有权选择推迟到 get() 时才执行 。想要真异步,就显式写 std::launch::async。


5. 异常怎么跨线程传

写端抛了异常怎么办?promise 不会自动捕获 ------ 但你可以把它"存进去",读端 get() 时会重新抛出来:

cpp 复制代码
#include <exception>
#include <future>
#include <iostream>
#include <stdexcept>
#include <thread>

int main()
{
    std::promise<int> p;
    auto f = p.get_future();

    std::thread t([&p] {
        try {
            throw std::runtime_error("sensor offline");
        } catch (...) {
            p.set_exception(std::current_exception());   // 把异常交给读端
        }
    });

    try {
        std::cout << f.get() << "\n";
    } catch (const std::exception& e) {
        std::cout << "caught: " << e.what() << "\n";     // caught: sensor offline
    }
    t.join();
}

这就是它优于手工同步的地方:线程边界上的异常能被正常地当作异常处理,而不是被吞掉或者让程序崩掉。

注意:如果写端什么都没设就析构了 (比如提前 return、抛了异常没接住),读端 get() 会抛 std::future_error,错误码是 broken_promise。这是标准库在帮你把"永久阻塞"变成"可捕获的异常"。


6. 等一会儿:wait_for / wait_until

不想无限等,就加超时:

cpp 复制代码
#include <chrono>
#include <future>
#include <iostream>
#include <thread>

int main()
{
    std::promise<int> p;
    auto f = p.get_future();
    std::thread t([&p] {
        std::this_thread::sleep_for(std::chrono::milliseconds(300));
        p.set_value(1);
    });

    using namespace std::chrono_literals;
    if (f.wait_for(100ms) == std::future_status::ready)
        std::cout << "value = " << f.get() << "\n";
    else
        std::cout << "timeout, 但我只等到这里\n";   // 走这条

    t.join();   // 任务还在跑,正常退出前得等它
}

关键认知:超时 ≠ 取消。

wait_for 返回 timeout 只表示"我不等了",后台那个任务照样在跑 ,还会把结果写进共享状态。标准库没有提供取消 std::future 的 API。

所以超时之后,你必须自己决定:表项谁清理、迟到的结果怎么办。(这是入门之后最容易出错的地方,完整版第 8、9 章专门讲。)

三个返回值:

wait_for 返回 含义
future_status::ready 结果已就绪,可以 get() 了
future_status::timeout 到点了还没好,任务还在跑
future_status::deferred 任务是推迟执行的,得 get() 或 wait() 才会跑

7. 一个结果给多个人:shared_future

std::future 的 get() 只能调一次。多个消费者要读同一个结果,用 std::shared_future:

cpp 复制代码
#include <future>
#include <iostream>
#include <thread>

int main()
{
    std::promise<int> p;
    std::shared_future<int> sf = p.get_future().share();   // 注意是 share()

    std::thread t([&p] { p.set_value(42); });

    std::thread a([sf] { std::cout << "A: " << sf.get() << "\n"; });
    std::thread b([sf] { std::cout << "B: " << sf.get() << "\n"; });

    t.join(); a.join(); b.join();
}

sf.get() 可以反复调用(线程安全,返回值拷贝),两个线程都拿到 42。注意捕获的是 sf 的拷贝 ------ 每个线程各有自己的 shared_future 副本,共享底层状态。


8. 追问:std::async 能替代 promise 吗?

只能替代一部分,不能整体替代。 分界线是一句话:

结果是不是"任务函数返回时"自然产生的?是 → std::async 能替代,而且更省事。不是 → 替代不了。

本质差别:写端在谁手上

promise 的核心能力不是"设置结果",而是写端可以被移交:在 A 线程创建它,塞进一张请求表,最后由 C 线程(IO 线程、回调线程、超时扫描线程)来填结果。

而 std::async 的写端是它内部造的,你拿不到 ------ 结果只能由那个函数 return 或 throw 产生。

这个差别在"超时"场景下会变成实打实的耗时差距(任务都是 500ms,超时都设 100ms):

cpp 复制代码
#include <chrono>
#include <exception>
#include <future>
#include <iostream>
#include <stdexcept>
#include <thread>

using Clock = std::chrono::steady_clock;
using namespace std::chrono_literals;

int main()
{
    // A:promise ------ 超时后能主动叫醒等待方(写端在手上)
    {
        std::promise<int> p;
        auto f = p.get_future();
        std::thread worker([&p] {
            std::this_thread::sleep_for(500ms);
            try { p.set_value(1); }
            catch (const std::future_error&) { /* 已被超时路径先手关闭,正常 */ }
        });

        const auto t0 = Clock::now();
        if (f.wait_for(100ms) == std::future_status::timeout) {
            // 关键:把"超时"这个结果投递进去
            p.set_exception(std::make_exception_ptr(std::runtime_error("timed out")));
            try { (void)f.get(); } catch (const std::exception&) {}
        }
        std::cout << "A(promise): "
                  << std::chrono::duration_cast<std::chrono::milliseconds>(Clock::now() - t0).count()
                  << " ms\n";
        worker.join();          // 后台线程仍在收尾(不计入上面的数字)
    }

    // B:async ------ 没有写端,只能干等任务跑完
    {
        auto f = std::async(std::launch::async, [] {
            std::this_thread::sleep_for(500ms);
            return 1;
        });

        const auto t0 = Clock::now();
        if (f.wait_for(100ms) == std::future_status::timeout) {
            (void)f.get();      // 没有任何 API 能让它立刻就绪
        }
        std::cout << "B(async)  : "
                  << std::chrono::duration_cast<std::chrono::milliseconds>(Clock::now() - t0).count()
                  << " ms\n";
    }
}

输出(实测):

text 复制代码
A(promise): 100 ms
B(async)  : 500 ms

A 之所以是 100ms,是因为写端在手上 ------ set_exception() 把"超时"当作结果投递了进去。B 连写端都没有,"超时"这件事根本无法表达给等待方,只能眼睁睁等任务跑完。

能力对照

能力 promise + future std::async
结果由谁产生 任意线程、任意时刻 只由任务函数 return/throw
写端能否移交 ✅ 核心能力 ❌ 拿不到
超时后立刻唤醒等待者 ✅ 实测 100 ms ❌ 实测 500 ms
任务句柄(取消/转移/查询) 自己管 ❌ 没有
线程模型 自己定,可复用线程池 每次调用可能新建线程
临时 future 析构 不阻塞 ⚠️ 阻塞到任务结束
默认策略 --- ⚠️ 可能是 deferred

能替代的部分

"后台算个返回值、我要等它"这一类,是纯粹的简化,直接换:

cpp 复制代码
// 旧:promise + 手工线程,只为算个值
std::promise<int> p;
auto f = p.get_future();
std::thread t([&p] { p.set_value(compute()); });
const int v = f.get();
t.join();

// 新:语义等价,短得多
auto f = std::async(std::launch::async, compute);
const int v = f.get();

两个前提别漏:显式写 std::launch::async (默认允许推迟执行),返回值必须被接住(临时 future 析构会阻塞)。

替代不了的部分

场景 为什么不行
RPC 客户端收响应 填结果的是 IO 线程,必须有可移交的写端
超时后主动关闭共享状态 没有写端 → 只能等到任务结束,超时形同虚设
线程池 / 限流 std::async 每次可能新建线程,不是池
一个结果给多方读 得先 .share(),而 async 没有可共享的登记点
需要真正取消任务 没有任务句柄,"取消"没地方表达

想"两全":packaged_task

既要 std::async 的"自动填结果",又要 promise 的"写端可控",用 std::packaged_task:

cpp 复制代码
std::packaged_task<int()> task(compute);
auto f = task.get_future();       // 读端可以像 promise 一样发出去
pool.submit(std::move(task));     // 但任务跑在哪儿由你决定
const int v = f.get();

决策规则

  1. 结果 = 函数返回值,且你愿意等它跑完 → std::async(std::launch::async, f),记得接住 future。
  2. 结果由别的地方、别的时刻产生 (RPC、回调、超时、事件) → promise + future。
  3. 想两者兼得 (自动填结果 + 任务投给自己的池子) → packaged_task + 线程池。

9. 速查与建议

基本流程

text 复制代码
promise.get_future()  →  把 future 交给消费者
promise.set_value(v)  /  set_exception(e)   →  定终局(一次)
future.get()          →  阻塞取值(一次,会取走)

用哪个?

场景 用什么
一个异步操作的结果 std::promise + std::future
结果要给多方读 std::shared_future(.share())
就想异步算个值、懒得管线程 std::async(std::launch::async, f),记得接住返回值
有返回值的一批任务 packaged_task + 线程池
真要能中断任务 C++20 std::jthread + std::stop_token
只等完成、不要值 std::latch / std::call_once

四条容易踩的坑

  1. 漏接 std::async 的返回值 ------ 那行变成同步等待。
  2. 忘记 std::async 默认允许 deferred ------ 显式写 launch::async。
  3. 以为 wait_for 超时能取消任务 ------ 它不能,超时后你得自己收尾。
  4. 没人 get() 的 future ------ 里面的异常会被安静地丢掉,程序不崩,你也永远不知道出了问题。

最后一句 :future / promise 本身很简单,难的是围绕它的纪律 ------ 谁登记、谁清理、谁负责关闭。这四条坑里有三条都属于"纪律"问题,而不是 API 用法问题。

相关推荐
特立独行的猫A1 小时前
C++ 异步编程:std::future 与 std::promise 详解(含 RPC 客户端实现实践)
c++·后端
All for pursuit.1 小时前
【分治-2】215.数组中的第K个最大元素
数据结构·c++·算法·leetcode
lcj25111 小时前
【C++】set和map——详细使用说明
开发语言·c++·笔记·面试
潼心1412o1 小时前
C++初阶(长期更新)第3讲:类和对象(中)
开发语言·c++
程序喵大人2 小时前
【C++入门】编译链接模型 - 02 预处理把头文件怎样塞进源文件
开发语言·c++·预处理·编译链接·头文件·源文件
艾莉丝努力练剑2 小时前
【AI大模型接入SDK】ChatSDK 集成测试概述
jvm·c++·人工智能·学习·面试·集成测试·sdk
艾莉丝努力练剑2 小时前
【AI大模型接入SDK】ChatSDK:CMake构建静态库完整实现
java·开发语言·网络·c++·人工智能·学习·sdk
東隅已逝,桑榆非晚3 小时前
stack和queue(类函数学习和模拟实现)
c++·笔记·学习
.道阻且长.3 小时前
C++11 :右值引用的作用,引用折叠和完美转发
java·开发语言·c++