Using std::promise for Asynchronous Results in C++
This article provides an overview of std::promise in
C++, explaining its fundamental role in multithreaded programming. It
explores how std::promise acts as the producer endpoint in
asynchronous communication, how it pairs with std::future
to create a synchronization channel, and how it enables threads to pass
both computed values and unhandled exceptions safely across thread
boundaries without manual locking primitives.
The Core Role of std::promise
In C++ concurrency, std::promise provides a mechanism to
store a value or an exception that is later acquired asynchronously via
a std::future object. Together, a std::promise
and a std::future form a single-use communication
channel:
std::promise(The Producer): Owned by the thread that produces the result. It writes data to the shared state.std::future(The Consumer): Owned by the thread waiting for the result. It reads data from the shared state.
While high-level utilities like std::async automatically
manage this relationship behind the scenes, std::promise
gives developers explicit control over when, where, and how a result is
made available.
Explicit and Decoupled Synchronization
The primary advantage of std::promise is decoupling
result production from thread execution. A thread does not need to
terminate to yield its result. A worker thread can initialize a
calculation, satisfy the promise midway through its execution by calling
set_value(), and then continue performing other
non-dependent operations.
#include <iostream>
#include <thread>
#include <future>
void computeValue(std::promise<int> resultPromise) {
// Perform intensive task
int result = 42;
// Make the result available to the waiting thread immediately
resultPromise.set_value(result);
// The thread can continue running background cleanup or logging
}
int main() {
std::promise<int> myPromise;
std::future<int> myFuture = myPromise.get_future();
std::thread worker(computeValue, std::move(myPromise));
// Blocks until resultPromise.set_value() is called
std::cout << "Result: " << myFuture.get() << std::endl;
worker.join();
return 0;
}Exception Propagation Across Threads
Standard C++ threads cannot naturally throw exceptions across
execution contexts; an uncaught exception on a child thread typically
triggers std::terminate.
std::promise solves this problem with the
set_exception() method. By capturing an active exception
pointer via std::current_exception(), a worker thread can
store the failure inside the promise. When the consumer thread calls
.get() on its associated std::future, the
stored exception is re-thrown in the context of the consumer thread,
allowing standard try-catch exception handling:
void riskyTask(std::promise<int> promise) {
try {
throw std::runtime_error("Calculation failed");
} catch (...) {
promise.set_exception(std::current_exception());
}
}Replacing Low-Level Primitives
Before the introduction of std::promise in C++11,
passing a single result between threads required combining a shared
variable, a std::mutex, and a
std::condition_variable.
Using std::promise simplifies this pattern
significantly:
- No Manual Locking: Thread synchronization and memory visibility barriers are handled internally.
- Safety Guarantees: A promise can only be set once.
Attempting to set a value or exception multiple times automatically
throws a
std::future_errorwith the error codepromise_already_satisfied. - Broken Promise Detection: If a
std::promiseis destroyed before setting a value, its associatedstd::futurewill throw astd::future_errorwithbroken_promise, preventing the consumer thread from blocking indefinitely.