C++ std::promise深度解析

std::promise是C++11异步模型中的生产者端点,与std::future共享状态,实现线程间一次性结果传递。它通过set_value或set_exception写入结果或异常,get_future()获取消费者视图,get()阻塞等待结果。对象不可拷贝,只能移动,确保唯一所有权。适用于单次结果返回、跨线程异常传播及事件信号,但不支持多生产/消费或频繁通信。其底层机制被std::async和std::packaged_task封装,是手动控制异步结果的理想选择。


一、状态机模型

复制代码
Producer Thread                  Shared State                 Consumer Thread
──────────────                  ─────────────                ──────────────
std::promise                       值 / 异常                  std::future
   set_value / set_exception  ───▶   ready = true   ───▶        get / wait

核心性质:

  • promise/future 是一对一次性通信通道

  • 共享状态可以持有:

    • 一个 T类型的值

    • 一个异常(std::exception_ptr)

    • 或者最终变成 broken_promise

  • set_value/ set_exception会使共享状态变为 ready

  • future::get()阻塞直到共享状态 ready,并取走结果或重抛异常


二、对象不可拷贝,只能移动

复制代码
std::promise<int> p;
std::promise<int> q = p;   // 编译错误:拷贝构造被删除
std::promise<int> r = std::move(p); // OK,p 此后无共享状态

规则:

  • 拷贝构造 / 拷贝赋值:被删除

  • 移动构造 / 移动赋值:转移共享状态所有权

  • 一个 promise 只能 get_future()一次

  • 一个共享状态只能由一个 promise 写入


三、核心成员函数

3.1 get_future()

复制代码
std::promise<int> p;
std::future<int> f = p.get_future();

语义:

  • 返回与当前 promise 共享同一共享状态的 future

  • 第二次调用抛出 std::future_error,错误码 future_already_retrieved

  • promise 被 move 走后,原对象无共享状态,调用会抛 no_state

顺序要求 :通常先 get_future(),再把 promise move 给工作线程。


3.2 set_value

复制代码
p.set_value(42);

语义:

  1. 将值写入共享状态

  2. 原子地使共享状态变为 ready

  3. 唤醒所有在 future::get/wait上阻塞的线程

约束:

  • 只能调用一次

  • 再次调用抛 std::future_error,错误码 promise_already_satisfied

  • 若 promise 无共享状态,抛 no_state

  • 值类型会按值/引用/右值规则拷贝或移动构造

引用特化:

复制代码
std::promise<const std::string&> p;
std::string s = "data";
p.set_value(s); // 存的是引用,调用方必须保证生命周期

void特化:

复制代码
std::promise<void> p;
p.set_value(); // 仅作"事件已发生"的信号

3.3 set_exception

复制代码
try {
    do_work();
    p.set_value(result);
} catch (...) {
    p.set_exception(std::current_exception());
}

语义:

  • 将 std::exception_ptr存入共享状态

  • 共享状态变为 ready

  • 消费者调用 future.get()时,异常会在消费者线程被重新抛出

这是 C++ 线程间安全传递异常的标准方式。不用 promise 时,工作线程未捕获异常会直接 std::terminate。


3.4 set_value_at_thread_exit

复制代码
p.set_value_at_thread_exit(42);

语义:

  • 立即把值放进共享状态,但不马上置为 ready

  • 当前线程退出、所有线程局部对象析构完成后,才把共享状态标记为 ready

  • 消费者会一直阻塞到生产者线程结束

适用场景:

  • 结果依赖线程局部存储(TLS)

  • 希望"线程真正收尾后再让别的线程看到结果"

  • 避免消费者拿到结果后,生产者线程的 TLS 还没析构造成逻辑错误

对应异常版本:

复制代码
p.set_exception_at_thread_exit(std::current_exception());

四、析构与 broken_promise

如果 promise 被销毁,而共享状态仍然没有被 set:

  • 标准规定:promise 放弃共享状态时,会向共享状态写入

    std::future_error(std::future_errc::broken_promise)

  • 共享状态变为 ready

  • 消费者 future.get()会抛出该异常,而不是永久阻塞

    std::future f;
    {
    std::promise p;
    f = p.get_future();
    // p 析构,但未 set_value
    }
    f.get(); // 抛 std::future_error(broken_promise)

工程含义:

promise 的所有权必须交给"真正会兑现结果"的执行流。

错误示例:

复制代码
void worker(std::promise<int>& p) {
    // 如果某些分支没 set_value,p 析构时 broken_promise
}

更好:

复制代码
void worker(std::promise<int> p) {
    try {
        p.set_value(compute());
    } catch (...) {
        p.set_exception(std::current_exception());
    }
}

五、线程安全性

标准保证:

  • 对同一个共享状态的内部读写是同步的

  • set_value与 future::get之间存在 happens-before 关系

  • set_value、set_exception、set_value_at_thread_exit、set_exception_at_thread_exit在更新 promise 对象时,表现得像获取了与该 promise 关联的同一个互斥体

但是:

多个线程同时调用同一个 promise 对象 的 set_value是未定义/非法用法。

正确模型:

  • 一个 promise:单一生产者

  • 一个 future:单一消费者

  • 多消费者请用 std::shared_future

    std::promise p;
    std::shared_future sf = p.get_future().share();

    std::thread a([sf]{ std::cout << sf.get(); });
    std::thread b([sf]{ std::cout << sf.get(); });


六、与 std::async/ std::packaged_task的关系

三者底层都是 promise/future:

工具 谁创建 promise 适用场景
std::promise 用户手动 非函数式、事件驱动、手动通知
std::packaged_task 包装可调用对象 把"函数对象"当任务投递给线程池
std::async 标准库内部 一次性异步函数调用

std::packaged_task本质:

复制代码
struct packaged_task {
    promise<R> p;
    F f;
    void operator()(Args... args) {
        try {
            p.set_value(f(args...));
        } catch (...) {
            p.set_exception(std::current_exception());
        }
    }
};

七、典型用法

7.1 线程返回结果

复制代码
void worker(std::promise<int> p) {
    p.set_value(heavy_compute());
}

std::promise<int> p;
std::future<int> f = p.get_future();

std::thread t(worker, std::move(p));
int result = f.get();
t.join();

7.2 跨线程异常传播

复制代码
void worker(std::promise<double> p) {
    try {
        double v = risky();
        p.set_value(v);
    } catch (...) {
        p.set_exception(std::current_exception());
    }
}

auto f = p.get_future();
try {
    double v = f.get();
} catch (const std::runtime_error& e) {
    // 处理工作线程抛出的异常
}

7.3 用 void promise 做一次性信号

复制代码
std::promise<void> ready;
std::future<void> f = ready.get_future();

std::thread producer([&ready] {
    prepare_resources();
    ready.set_value(); // 发信号:资源准备好了
});

f.wait(); // 消费者等资源就绪
start_consuming();

这比 mutex + condition_variable简洁,但只能通知一次。


八、常见错误

8.1 多次 set

复制代码
p.set_value(1);
p.set_value(2); // 抛 promise_already_satisfied

8.2 promise 生命周期短于工作线程

复制代码
std::future<int> f;
std::thread t;
{
    std::promise<int> p;
    f = p.get_future();
    t = std::thread([](std::promise<int> p){
        p.set_value(1);
    }, std::move(p));
}
t.join();

上面这段其实没问题,因为 promise 已经 move 进线程了。

真正危险的是:

复制代码
std::promise<int> p;
auto f = p.get_future();
std::thread([&p]{ p.set_value(1); }).detach();
// 函数返回,p 可能已析构,线程还在跑 → UB / broken_promise

规则:

promise 要么 move 进线程,要么生命周期由智能指针/容器管理。


8.3 把 promise 当队列用

promise/future 是一次性的:

  • 不适合多次生产/消费

  • 不适合事件流

  • 不适合管道消息

需要这些能力时,用:

  • std::queue + mutex + condition_variable

  • 无锁队列

  • 线程池任务队列


九、适用边界

适合:

  • 一个任务产出一个结果

  • 工作线程不是简单函数调用,而是事件/回调/状态机

  • 需要手动控制"什么时候兑现结果"

  • 需要跨线程传异常

不适合:

  • 高频小任务(用线程池)

  • 多生产者多消费者消息流(用队列)

  • 多次通知(用 condition_variable / channel)

  • 简单异步函数调用(用 std::async)


十、一句话总结

std::promise是"我以后会给你一个结果"的契约对象;

set_value/ set_exception是履约;

future.get()是等待履约;

不履约,就是 broken_promise。

它是 C++ 并发模型里比 std::thread抽象、比 std::async底层的那一层:

当你需要"手动兑现结果"时,用它;当你只想要"函数异步返回值"时,别直接用它。

相关推荐
青少儿编程课堂2 小时前
迪杰斯特拉(Dijkstra)算法:单源最短路与赛场路线规划
c++·python·算法·bfs·信息学竞赛
沫璃染墨3 小时前
《Qt从零入门系列(十三):QSS样式表详解——从选择器与层叠规则到控件美化实战》
开发语言·css·c++·qt·ui·qt6.3·qss
道尔柯南4 小时前
C++ 面向对象编程入门:类和对象(上)
开发语言·c++·算法
aqiu1111116 小时前
【C++算法打怪专栏】C++ `unordered_set` 常用初始化与插入方法总结
开发语言·c++·算法
Terra.K6 小时前
高精度阶乘求和模板深度解析
开发语言·c++·算法
智码看视界6 小时前
C++智能指针全景剖析及面试20问
开发语言·c++·深度学习·面试·内存管理·智能指针
Peregrine97 小时前
C++ - > 类和对象
java·c++·算法
‎ദ്ദിᵔ.˛.ᵔ₎8 小时前
c++11:fuction包装器
开发语言·c++
霍霍的袁9 小时前
【C++】哈希表实现指南:从原理到开放定址法、链地址法的实现
c++·学习·哈希算法·散列表·visual studio