std::async, std::future & std::promise
Use higher-level concurrency abstractions: launch asynchronous tasks with std::async, retrieve results with std::future, and manually set values or exceptions with std::promise.
Task-Based Concurrency
While std::thread gives you low-level control, task-based concurrency with std::async and std::future is often easier and less error-prone. Instead of manually managing threads and shared state, you express work as tasks that produce results. The runtime decides how to schedule them.
A std::future represents a value of type T that will be available in the future. You can call get() to block until the result is ready. If the task threw an exception, get() rethrows it — giving you automatic exception propagation across threads.
std::async — Launching Asynchronous Tasks
std::async launches a callable and returns a std::future holding the result. You can specify a launch policy: std::launch::async forces a new thread, while std::launch::deferred delays execution until get() is called.
#include <iostream>
#include <future>
#include <chrono>
#include <numeric>
#include <vector>
long long parallel_sum(const std::vector<int>& v, size_t start, size_t end) {
if (end - start < 1000) {
return std::accumulate(v.begin() + start, v.begin() + end, 0LL);
}
size_t mid = start + (end - start) / 2;
// Launch left half asynchronously
auto left = std::async(std::launch::async,
parallel_sum, std::cref(v), start, mid);
// Compute right half in this thread
long long right_sum = parallel_sum(v, mid, end);
return left.get() + right_sum; // .get() blocks until left is done
}
int main() {
std::vector<int> data(1'000'000, 1);
auto start = std::chrono::high_resolution_clock::now();
long long result = parallel_sum(data, 0, data.size());
auto end = std::chrono::high_resolution_clock::now();
auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
std::cout << "Sum: " << result << " in " << ms.count() << "ms\n";
return 0;
}Launch Policies: async vs deferred
std::launch::async — The function is executed on a new thread (or one from a thread pool, implementation-defined). Work begins immediately.
std::launch::deferred — The function is not executed until get() or wait() is called on the future. It runs in the calling thread (lazy evaluation). Useful for computations you may not need.
Default (no policy specified) — The implementation chooses. This can be async, deferred, or both (meaning it decides at runtime). If you need guaranteed parallelism, always specify std::launch::async.
std::promise — Manual Result Setting
std::promise is the write end of a future-promise channel. You create a promise, extract its future, and then either set_value() or set_exception() from any thread.
#include <iostream>
#include <thread>
#include <future>
#include <stdexcept>
void compute(std::promise<int> prom, int x) {
try {
if (x < 0) {
throw std::invalid_argument("negative input");
}
int result = x * x;
prom.set_value(result); // fulfil the promise
} catch (...) {
prom.set_exception(std::current_exception()); // propagate error
}
}
int main() {
std::promise<int> prom;
std::future<int> fut = prom.get_future(); // get the read end
std::thread t(compute, std::move(prom), 7);
try {
int result = fut.get(); // blocks until value or exception
std::cout << "Result: " << result << "\n"; // prints 49
} catch (const std::exception& e) {
std::cout << "Error: " << e.what() << "\n";
}
t.join();
return 0;
}std::shared_future — Multiple Consumers
A std::future can only be get()-ed once (it moves the value out). std::shared_future allows multiple threads to wait for and read the same result.
#include <iostream>
#include <future>
#include <thread>
#include <vector>
int main() {
auto shared = std::async(std::launch::async, []() {
std::this_thread::sleep_for(std::chrono::seconds(1));
return 42;
}).share(); // convert future to shared_future
// Multiple threads can all read the same result
std::vector<std::thread> consumers;
for (int i = 0; i < 5; ++i) {
consumers.emplace_back([shared, i]() {
int val = shared.get(); // all 5 threads get 42
std::cout << "Consumer " << i << " got: " << val << "\n";
});
}
for (auto& t : consumers) t.join();
return 0;
}A std::future returned by std::async has a special property: its destructor blocks until the task completes (like an implicit join). This means if you discard the returned future, the "async" call becomes effectively synchronous:
```cpp
// SURPRISE — this blocks! The temporary future is destroyed
// at the semicolon, which waits for the task to finish.
std::async(std::launch::async, long_computation);
// CORRECT — store the future to let work proceed in parallel
auto fut = std::async(std::launch::async, long_computation);
// ... do other work ...
fut.get(); // now wait for the result
```
This is the most common pitfall with std::async. Always capture the returned std::future.
std::packaged_task
std::packaged_task wraps a callable and provides a std::future for its result. Unlike std::async, it does not automatically run the task — you invoke it manually (typically by moving it into a thread or a task queue). This makes it ideal for building custom thread pools:
1. Wrap work in a std::packaged_task.
2. Extract the std::future and give it to the caller.
3. Move the task into a work queue.
4. Worker threads pull tasks from the queue and invoke them.
Use std::async when you want a result from concurrent work and don't need fine-grained thread control. It automatically propagates exceptions and manages thread lifetimes. Use std::thread (or std::jthread) when you need persistent background workers, custom scheduling, or when you're building infrastructure like thread pools. For fire-and-forget work, always keep the returned std::future alive to avoid accidental blocking.
std::asynclaunches a task and returns astd::future— a high-level alternative to manual thread managementstd::launch::asyncforces a new thread;std::launch::deferreddelays untilget()is calledstd::promiseis the write end of a future channel — use it for manual value/exception settingstd::shared_futurelets multiple threads read the same result- The
std::futurefromstd::asyncblocks in its destructor — always store it in a variable std::packaged_taskwraps a callable with a future, ideal for custom thread pools
Quiz — Test Your Knowledge
(15 XP)1. What happens if you discard the `std::future` returned by `std::async(std::launch::async, fn)`?
2. What is the difference between `std::future` and `std::shared_future`?
3. How does `std::async` propagate exceptions from the task to the caller?