C++ / CONCURRENCY AND PARALLELISM
async, futures, and returning values from threads
Return values and exceptions from worker threads using std::async, std::future, and std::promise, and pick launch policies on purpose.
What you will learn
- Return values and exceptions out of a thread using std::async and future::get
- Pair std::promise with std::future to return a result from a plain std::thread
- Choose std::launch::async or deferred deliberately instead of the default policy
- Store every future in a named variable so the destructor does not serialize your tasks
Understanding async, futures, and returning values from threads
std::thread discards whatever its callable returns, which is why thread-based code tends to write results into a captured variable and then needs a mutex to make that write safe. std::async takes a different route: it wraps the call in a shared state, runs it, and stores either the returned value or the exception that escaped. std::future is the read end of that shared state, and get() blocks until the state is ready and then moves the result out. The two threads never share a mutable variable, so no lock appears anywhere in the code; the result is handed over exactly once.
A future is one-shot on purpose. get() moves the stored value out and leaves valid() == false, so a second get() on the same future is undefined behaviour, which libstdc++ and libc++ report by throwing std::future_error with the no_state code; when several places need the same result, call share() to turn it into a std::shared_future, whose get() returns a const reference and may be called repeatedly. Exceptions travel the same channel as values: whatever the task throws is captured into the shared state and rethrown by get() on the waiting thread. That matters because an exception escaping a plain thread function calls std::terminate, so a future is the ordinary way to move a failure across a thread boundary.
The argument people leave out is the launch policy. std::launch::async requires the task to run on a new thread; std::launch::deferred means it does not run at all until get() or wait() is called, and then it runs on the calling thread; the default is the union of both, so the implementation may choose either. Lifetime is the second surprise: the future returned by std::async has a destructor that waits for the task to finish, so a call whose future is never stored in a variable is a synchronous call in disguise. Futures from std::promise and std::packaged_task have no such destructor behaviour.
<functional>
<future>
<iostream>
<stdexcept>
<vector>
long long sum(const std::vector<int>& v, std::size_t from, std::size_t to) {
long long total = 0;
for (std::size_t i = from; i < to; ++i)
total += v[i];
return total;
}
int doubled(int n) {
if (n < 0)
throw std::invalid_argument("negative input");
return 2 * n;
}
int main() {
std::vector<int> data(1000);
for (std::size_t i = 0; i < data.size(); ++i)
data[i] = static_cast<int>(i) + 1;
// Starts on another thread immediately; main keeps working.
std::future<long long> half =
std::async(std::launch::async, sum, std::cref(data), 0u, 500u);
long long rest = sum(data, 500, 1000);
std::cout << "sum 1..1000 = " << half.get() + rest << '\n';
// A task that throws: the exception is stored, not lost.
std::future<int> f = std::async(std::launch::async, doubled, -3);
try {
int value = f.get();
std::cout << "doubled: " << value << '\n';
} catch (const std::invalid_argument& e) {
std::cout << "exception crossed the thread: " << e.what() << '\n';
}
std::cout << std::boolalpha << "f still valid: " << f.valid() << '\n';
}
A future is the read end of a one-shot channel that carries either the call's return value or the exception it threw, and get() is the single point where that result becomes yours on the waiting thread.
Worked examples
A promise/future pair across a raw thread
Shows the value-or-exception channel on its own, filled by a std::thread instead of by std::async.
<exception>
<future>
<iostream>
<stdexcept>
<string>
<thread>
void load(std::promise<std::string> p, bool fail) {
if (fail) {
p.set_exception(std::make_exception_ptr(std::runtime_error("disk offline")));
return;
}
p.set_value("config loaded");
}
int main() {
std::promise<std::string> ok, bad;
std::future<std::string> f_ok = ok.get_future();
std::future<std::string> f_bad = bad.get_future();
std::thread t1(load, std::move(ok), false);
std::thread t2(load, std::move(bad), true);
std::cout << f_ok.get() << '\n';
try {
std::string s = f_bad.get();
std::cout << s << '\n';
} catch (const std::runtime_error& e) {
std::cout << "failed: " << e.what() << '\n';
}
t1.join();
t2.join();
std::cout << std::boolalpha << "f_ok valid after get: " << f_ok.valid() << '\n';
}
Example explained
Line 1get_future() must be called before the promise leaves main, because afterwards main owns only the read end.
Line 2std::promise is move-only, so std::move is required to hand it to the thread constructor.
Line 3set_exception stores an exception_ptr where a value would go, and f_bad.get() rethrows it in main.
Line 4f_ok.valid() is false afterwards: get() moved the string out of the shared state rather than copying it.
async versus deferred
Demonstrates that std::launch::deferred does not start any thread and runs the task inside get() on the calling thread.
<chrono>
<future>
<iostream>
<thread>
int slow(int id) {
std::this_thread::sleep_for(std::chrono::milliseconds(20));
std::cout << "task " << id << " ran\n";
return id * 10;
}
int main() {
std::future<int> eager = std::async(std::launch::async, slow, 1);
std::future<int> lazy = std::async(std::launch::deferred, slow, 2);
std::this_thread::sleep_for(std::chrono::milliseconds(200));
std::cout << "main woke up\n";
int a = eager.get(); // already finished, returns at once
std::cout << "eager -> " << a << '\n';
int b = lazy.get(); // slow(2) executes here, on this thread
std::cout << "lazy -> " << b << '\n';
}
Example explained
Line 1The eager task prints while main is sleeping, proving std::launch::async really spawned a thread.
Line 2The deferred task prints nothing during that sleep because no execution agent exists for it yet.
Line 3eager.get() does not block: the value was already stored in the shared state 180 ms earlier.
Line 4lazy.get() runs slow(2) synchronously, so 'task 2 ran' appears on the main thread just before its result.
Four tasks in flight at once
Keeps futures alive in a container so the tasks overlap, and collects the results in order.
<chrono>
<future>
<iostream>
<thread>
<vector>
int square(int n) {
std::this_thread::sleep_for(std::chrono::milliseconds(100));
return n * n;
}
int main() {
auto start = std::chrono::steady_clock::now();
std::vector<std::future<int>> tasks;
for (int i = 1; i <= 4; ++i)
tasks.push_back(std::async(std::launch::async, square, i));
int total = 0;
for (std::future<int>& t : tasks)
total += t.get();
auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now() - start).count();
std::cout << "total " << total << '\n';
std::cout << std::boolalpha << "finished in under 300 ms: " << (ms < 300) << '\n';
}
Example explained
Line 1push_back moves the future into the vector; futures are move-only, so a copy would not compile.
Line 2All four tasks are already running when the second loop starts, so the four 100 ms sleeps overlap.
Line 3Calling get() in index order is fine: waiting for task 1 does not delay tasks 2 to 4.
Line 4Writing std::async(std::launch::async, square, i); with no variable would push the run time to about 400 ms, because the discarded future waits at the semicolon.
Important notes
On GCC and Clang with libstdc++, compile with -pthread; without it std::async(std::launch::async, ...) throws std::system_error at run time rather than starting a thread.
If a std::promise is destroyed before set_value or set_exception is called, the waiting get() throws std::future_error with the broken_promise code, which is how an abandoned worker surfaces to the consumer.
Common mistakes
Writing std::async(std::launch::async, work, i); as a bare statement in a loop: the temporary future dies at the semicolon and its destructor waits for the task, so a supposedly parallel loop runs strictly one task at a time.
Calling get() twice on the same std::future: the first call moves the result out and leaves the future invalid, and the second is undefined behaviour that libstdc++ and libc++ turn into a std::future_error saying there is no associated state.
Relying on the default launch policy and never calling get() or wait(): if the implementation chose deferred, the task never runs at all, so side effects such as writing a log line silently disappear.
Try it yourself
Change, predict, then run
Write long long digit_sum(long long n) that throws std::domain_error for negative n, launch it with std::async(std::launch::async, ...) on 987654321 and on -5, and print the first result along with the message caught from the second future's get().
Open the C++ workspaceCheck your understanding
A loop calls std::async(std::launch::async, work, i); four times without storing the returned value, and each work call sleeps 100 ms. The loop takes about 400 ms. Why?
- std::async reuses one internal worker thread, so the four tasks queue behind each other
- Each returned future is a temporary that dies at the semicolon, and the destructor of a future from std::async waits for its task to finish
- The tasks ran deferred, so each one executed inline on the main thread at the point of creation
- sleep_for on a spawned thread is charged to the thread that created it, so the sleeps add up
Show answer
std::async always returns a future, and an unnamed one is destroyed at the end of the full expression; that specific destructor blocks until the shared state is ready, so each iteration finishes its task before the next begins. Option 3 is tempting, but std::launch::async explicitly forbids deferred execution, and a deferred task would not run at the creation point either — it would only run inside get() or wait(), which this loop never calls.