在C++多线程编程中,线程之间除了共享内存加锁同步,还需要一种简洁的机制来传递一次性的异步结果。比如工作线程完成后把计算结果交给等待的主线程,而不必反复检查共享变量。标准库中的 std::promise 与 std::future 就是为这个场景设计的搭配:一个负责写入,一个负责读取,二者通过内部的共享状态安全地完成值的传递。

一、promise与future的基本协作模型
理解这对组件的关键在于它们之间的共享状态。std::promise 本身并不直接存储结果,而是在调用 get_future 时创建一个内部共享状态对象,并返回与之关联的 std::future。之后工作线程可以在任意时刻调用 set_value 或 set_exception 把结果写入共享状态,而等待线程则通过 get 从共享状态读取。整个过程不需要额外的锁或条件变量,标准库已经处理好了同步与可见性问题。
这种设计有一个重要特性:值只能被传递一次。一旦 set_value 成功,共享状态就进入就绪状态,后续再次调用 set_value 会抛出 std::future_error。同样,future::get 也只能调用一次,第二次调用会触发未定义行为或异常。因此在使用时要确保一个结果只写入一次、只读取一次。如果需要多个线程读取同一结果,则需要使用 std::shared_future,这个后面会讲到。
另一个需要注意的地方是 std::promise 和 std::future 都是只可移动类型,不能拷贝。这是因为共享状态的所有权必须唯一,否则会出现双重释放或状态竞争。下面的代码展示了最基本的用法:工作线程接收一个移动后的 std::promise,计算完成后写入结果;主线程保留关联的 std::future 并阻塞等待结果。
#include <iostream>
#include <thread>
#include <future>
void compute(std::promise<int> prom) {
int result = 42;
prom.set_value(result);
}
int main() {
std::promise<int> prom;
std::future<int> fut = prom.get_future();
std::thread t(compute, std::move(prom));
std::cout << "结果: " << fut.get() << std::endl;
t.join();
return 0;
}
在这段代码里,如果工作线程尚未完成 set_value,主线程调用 fut.get 时会阻塞,直到结果就绪。这正是异步结果获取所需要的语义:调用方不必轮询状态,可以高效地等待数据到达。
二、std::async与std::packaged_task的辅助用法
手动创建 std::promise 并把它移动到线程中虽然直观,但在很多场景下显得繁琐。C++11 还提供了 std::async,它内部封装了线程创建、promise 与 future 的关联以及结果传递。调用 std::async 时会立即返回一个 std::future,任务函数在后台线程执行,返回值自动通过共享状态传递给 future。
使用 std::async 的好处是代码更简洁,且可以指定启动策略。例如 std::launch::async 表示必须异步执行,std::launch::deferred 表示延迟到调用 get 时再执行。如果不指定策略,具体行为由实现决定。下例展示了异步执行一个加法函数并获取返回值:
#include <future>
#include <iostream>
int add(int a, int b) {
return a + b;
}
int main() {
std::future<int> fut = std::async(std::launch::async, add, 10, 20);
std::cout << "异步结果: " << fut.get() << std::endl;
return 0;
}
还有一种常见工具是 std::packaged_task。它可以把一个可调用对象包装起来,并暴露关联的 std::future。与 std::async 不同,packaged_task 本身不创建线程,它只是把任务与 future 绑定,线程调度由使用者控制。这非常适合线程池、任务队列等需要自定义执行策略的场景。下面的例子中,我们创建一个 packaged_task,将其移动到线程中执行,并从 future 中获取结果。
#include <future>
#include <iostream>
#include <thread>
int multiply(int a, int b) {
return a * b;
}
int main() {
std::packaged_task<int(int, int)> task(multiply);
std::future<int> fut = task.get_future();
std::thread t(std::move(task), 6, 7);
std::cout << "乘积: " << fut.get() << std::endl;
t.join();
return 0;
}
选择哪种方式取决于控制粒度。如果只是简单地启动一个后台任务并等待结果,std::async 最省事;如果需要把任务投递到已有的线程池,或者要对可调用对象进行更细粒度的管理,std::packaged_task 更合适;而直接使用 std::promise 则提供了最大的灵活性,适合在已有线程函数中传递结果。
三、异常传播与shared_future多消费者场景
异步计算常常也会产生异常。如果工作线程内部抛出了异常,不能简单地让异常跨线程传播,那样会直接终止程序。标准机制是使用 promise::set_exception 把异常指针写入共享状态,然后在调用 future::get 时重新抛出这个异常。典型的写法是在工作线程中用 try/catch 捕获异常,并调用 set_exception(std::current_exception())。下面的代码演示了异常从工作线程传递到主线程的过程。
#include <future>
#include <iostream>
#include <thread>
#include <exception>
void fail(std::promise<int> prom) {
try {
throw std::runtime_error("计算失败");
} catch (...) {
prom.set_exception(std::current_exception());
}
}
int main() {
std::promise<int> prom;
std::future<int> fut = prom.get_future();
std::thread t(fail, std::move(prom));
try {
fut.get();
} catch (const std::exception& e) {
std::cerr << "捕获异常: " << e.what() << std::endl;
}
t.join();
return 0;
}
当多个线程都需要读取同一个异步结果时,普通的 std::future 无法满足要求,因为 get 只能调用一次。此时可以使用 std::shared_future,它允许多个副本同时访问共享状态,并且每个副本都可以多次调用 get。可以通过 future::share 方法把一个 std::future 转换为 std::shared_future。下面的例子中,主线程把结果共享给三个读取线程,每个线程都能安全地获取到同一个值。
#include <future>
#include <iostream>
#include <thread>
#include <vector>
int main() {
std::promise<int> prom;
std::shared_future<int> sf = prom.get_future().share();
std::thread t([&prom]() {
prom.set_value(100);
});
std::vector<std::thread> readers;
for (int i = 0; i < 3; ++i) {
readers.emplace_back([sf]() {
std::cout << "读取值: " << sf.get() << std::endl;
});
}
t.join();
for (auto& th : readers) {
th.join();
}
return 0;
}
使用 std::shared_future 时需要注意,多个线程同时调用 get 是安全的,但结果仍然只能被设置一次。调用 share 之后,原始的 std::future 会失效,不应该继续使用。
四、常见误用与最佳实践
在实际使用中,最常见的问题之一是工作线程忘记调用 set_value 或 set_exception。如果 std::promise 对象被析构时共享状态仍未就绪,那么等待在 future::get 上的线程会收到一个 std::future_error,错误码为 std::future_errc::broken_promise。这种情况通常发生在工作线程提前退出、异常未被捕获或者代码逻辑遗漏了写入操作。因此要保证所有路径都能写入结果,尤其是异常路径要调用 set_exception。
另一个容易忽视的问题是 future::get 的阻塞行为。如果任务函数内部存在死循环、长时间等待或者依赖了被阻塞的资源,主线程就会一直卡在 get 上。对于可能执行时间过长的任务,建议结合超时机制或条件变量做额外控制。例如可以先使用 future::wait_for 检查状态,再决定是否继续等待。虽然 get 本身没有超时参数,但可以通过 wait_for 避免无限阻塞。
此外,还应当注意 std::promise 和 std::future 的生命周期管理。由于它们是只可移动类型,传递时需要显式使用 std::move。如果误用了拷贝语义,编译器会直接报错,这在一定程度上避免了资源管理错误。在使用 std::async 时要小心返回的 std::future 对象被销毁的情况:如果析构时任务尚未执行完成,析构函数可能会阻塞等待任务结束。因此在需要异步执行时,最好明确指定 std::launch::async 并妥善保存 future 对象。
总结一下,std::promise 与 std::future 提供了一种简洁、安全的一次性异步结果传递机制。理解它们的共享状态模型、只可移动语义以及一次性读写限制,是正确使用它们的前提。配合 std::async、std::packaged_task 和 std::shared_future,可以覆盖大多数线程间通信与异步结果获取的需求。在编写多线程代码时,优先考虑这些标准组件,往往能减少手写锁和条件变量带来的复杂性与出错概率。
std::promisestd::futureC++异步编程修改时间:2026-10-06 08:55:54