Threaded Map (Part 3) - Futures, Promises and Async
In Part 2, we explored thread pools as an efficient way to manage concurrent tasks. However, one crucial aspect remained unaddressed: how do we handle return values from these tasks? Enter futures, promises, and async programming.
Future
Imagine you're ordering food from a restaurant. When you place your order, you get a receipt with an order number. This receipt is like a std::future - it's a promise that you'll get something later. You can check if your order is ready (.wait_for()
), wait until it's done (.get()
), or do other things while waiting.
There are two types of futures in C++:
- std::future: The Private Jet Model
- Exclusive access to the result
- Only one thread can retrieve the value
- Once .get() is called, the future is consumed
- Perfect for one-to-one async operations
std::future<int> calculateValue() {
return std::async([]() {
std::this_thread::sleep_for(std::chrono::seconds(2));
return 42;
});
}
int main() {
auto future = calculateValue();
// Do other work while value is being calculated
int result = future.get(); // Blocks until value is ready
std::cout << "Result: " << result << std::endl;
}
- std::shared_future: The Commercial Airline Model
- Multiple threads can access the same result
- Calling .get() doesn't consume the future
- Perfect for broadcast scenarios where multiple threads need the same result
std::shared_future<int> sharedCalculation() {
auto future = std::async([]() {
std::this_thread::sleep_for(std::chrono::seconds(2));
return 42;
});
return future.share(); // Convert to shared_future
}
void waitForResult(std::shared_future<int> future, int id) {
int result = future.get(); // Multiple threads can call get()
std::cout << "Thread " << id << " got result: " << result << std::endl;
}
int main() {
auto shared_future = sharedCalculation();
std::vector<std::thread> threads;
for (int i = 0; i < 3; ++i) {
threads.emplace_back(waitForResult, shared_future, i);
}
for (auto& t : threads) t.join();
}
Packaged Task
You can think of std::packaged_task
as an asynchronous std::function
that produces result via std::future
while std::function is a general purpose callable wrapper, std::packaged_task is a callable wrapper for asynchronous functions, it basically wraps an callable so that it can be invoked asynchronously or synchronously depending on where the task is called from. It lets you:
- Wraps a callable and binds it to a std::future.
- You manually invoke the task when you’re ready using task().
- Used when you want explicit control over the task’s execution.
std::packaged_task<int(int, int)> task([](int a, int b) {
std::this_thread::sleep_for(std::chrono::seconds(1));
return a + b;
});
// Get the future before running the task
std::future<int> result = task.get_future();
// Run the task in a separate thread
std::thread t(std::move(task), 2, 3);
t.detach();
// Wait for and get the result
std::cout << "Result: " << result.get() << std::endl;
Async
std::async
provides a high-level way to run a function asynchronously. It abstracts thread creation, lifecycle management, and result handling. This makes it an excellent choice when you want to execute a task asynchronously without the hassle of manually managing threads. Unlike std::packaged_task
, it automatically starts execution as soon as it's called.
- Takes a callable (function, lambda, etc.) and automatically runs it.
- Returns a std::future to access the result.
- Optionally supports deferred or immediate execution (std::launch::deferred or std::launch::async).
enum class TaskType { Fast, Slow };
std::future<std::string> processTask(TaskType type) {
// Launch policy can be:
// - std::launch::async (run in new thread)
// - std::launch::deferred (run when get() is called)
// - std::launch::async | std::launch::deferred (system decides)
return std::async(std::launch::async, [type]() {
if (type == TaskType::Slow) {
std::this_thread::sleep_for(std::chrono::seconds(2));
return std::string("Slow task completed");
} else {
return std::string("Fast task completed");
}
});
}
int main() {
auto slowTask = processTask(TaskType::Slow);
auto fastTask = processTask(TaskType::Fast);
// Fast task completes first
std::cout << fastTask.get() << std::endl;
std::cout << slowTask.get() << std::endl;
}
Promise
std::promise
represents the producer side of an asynchronous computation. While std::future lets you retrieve a result, a promise gives you manual control over how and when that result is produced (I know sounds like mumbo jumbo 😂).
Think of planning a birthday party with friends over a group chat:
- Jennifer promises to bring the cake (she creates a
std::promise
) - Everyone in the chat knows cake is coming (they have the
std::future
) - When Jennifer buys the cake, she updates the chat (she sets the promise's value)
- The party can't start until the cake arrives (other threads wait on the future)
// Jennifer planning the party
void jennifer_cake_duty(std::promise<std::string>&& cake_promise) {
try {
// Jennifer goes to the bakery
std::this_thread::sleep_for(std::chrono::seconds(2));
// She got a chocolate cake!
cake_promise.set_value("🎂 Chocolate Cake");
} catch (const BakeryClosedException& e) {
// Oh no, bakery was closed!
cake_promise.set_exception(std::current_exception());
}
}
// The party organizer
void party_organizer(std::future<std::string>&& cake_future) {
try {
std::cout << "Waiting for cake..." << std::endl;
std::string cake = cake_future.get(); // Waits for Jennifer
std::cout << "Great! " << cake << " has arrived! Party can start!" << std::endl;
} catch (const std::exception& e) {
std::cout << "Bad news: " << e.what() << std::endl;
std::cout << "Need a backup plan!" << std::endl;
}
}
How Each Tool Fits into the Broader Concurrency Ecosystem
std::future
: Provides a mechanism to retrieve the result of an asynchronous computation. Best used when there’s a one-to-one relationship between the producer and consumer of the result.std::shared_future
: Extends std::future for scenarios where multiple consumers need access to the same result, making it ideal for broadcast-style workflows.std::packaged_task
: Offers manual control over the execution of a callable. It is particularly useful when tasks need to be scheduled explicitly, decoupled from thread creation.std::promise
: Gives developers manual control over how a result is delivered to a future. Promises are ideal for coordinating tasks when the result depends on an external event or callback.std::async
: Provides a high-level abstraction that combines thread management and future handling in one package, making it the simplest tool for fire-and-forget async operations.
Remember, these tools aren't mutually exclusive - they often work best in combination, each handling the part of the problem they're best suited for.