C++ Module 16 – Concurrency: threads, atomics and the memory model
std::async, futures and promises
Run tasks with std::async and collect their results through futures, get exceptions back from other threads, and fulfil futures by hand with promises.
What you will learn
- Run tasks asynchronously with std::async and collect their results with std::future
- Catch a task's exception in the thread that reads its result
- Choose a launch policy and predict where and when a task runs
- Complete a future by hand with std::promise and std::packaged_task
- Avoid the blocking destructor of the futures that std::async returns
Before you start
What you’ll learn · Before you start
What you will learn
- Run tasks asynchronously with std::async and collect their results with std::future
- Catch a task's exception in the thread that reads its result
- Choose a launch policy and predict where and when a task runs
- Complete a future by hand with std::promise and std::packaged_task
- Avoid the blocking destructor of the futures that std::async returns
Before you start
On this page
Four downloads of 300, 200, 400 and 100 ms take 1.0 s one after the other and 0.4 s when they run at once, the time
of the slowest. std::async runs a function as a task, usually on another thread, and returns a
std::future: The reading end of a result that another thread or a deferred task produces: get() waits until the result is ready, then returns the value or throws the stored exception. A std::future can be read once.: a handle to a result that does not exist yet. Calling get() on the future
waits until the task has finished and then returns its value, or throws the exception: An object thrown with throw to report that a function cannot do what it was asked. Control leaves the function at once and passes up the chain of callers to the first handler whose type matches the object. More in “C++ exceptions: throw, catch and stack unwinding” the
task threw. Futures are how one thread hands a single result to another without sharing variables or locks.
When a result comes from code you run yourself instead of from std::async, promises do the writing: a
std::promise: The writing end of a shared state that your own code fulfils, once, with set_value() or set_exception(); get_future() gives the std::future that reads it. is the end you fulfil, and its future is the end you read, while
std::packaged_task turns any function into a task with a future. This lesson runs four simulated downloads at
once, sends an exception back through a future, shows where each launch policy: The first argument of std::async: std::launch::async runs the task as if on a new thread, at once; std::launch::deferred runs it on the caller, inside the first get() or wait(); without one, the library chooses. runs a
task, and fulfils a future by hand.
std::async starts each task at once, and get() waits for its result
Four “downloads” that only sleep, so the example needs no network, run first one after the other and then all at once with std::async:
// Four "downloads" that only sleep, so the example needs no network: first one after the other, then all at once with
// std::async. Each returns the size of the file it pretends to fetch.
#include <chrono>
#include <functional>
#include <future>
#include <print>
#include <string>
#include <thread>
#include <vector>
using namespace std::chrono_literals;
using Clock = std::chrono::steady_clock;
struct File {
std::string name;
std::chrono::milliseconds takes;
long bytes;
};
long download(const File& f) {
std::this_thread::sleep_for(f.takes); // stands in for waiting on the network
return f.bytes;
}
double seconds_since(Clock::time_point start) { return std::chrono::duration<double>(Clock::now() - start).count(); }
int main() {
const std::vector<File> files{{"a.csv", 300ms, 1200}, {"b.csv", 200ms, 3400}, {"c.csv", 400ms, 560}, {"d.csv", 100ms, 7800}};
auto start = Clock::now();
long total = 0;
for (const File& f : files) total += download(f);
std::println("one after the other: {} bytes in {:.1f} s", total, seconds_since(start));
start = Clock::now();
std::vector<std::future<long>> pending;
for (const File& f : files) pending.push_back(std::async(std::launch::async, download, std::cref(f)));
total = 0;
for (std::future<long>& p : pending) total += p.get(); // waits for each result in turn
std::println("with std::async: {} bytes in {:.1f} s", total, seconds_since(start));
} from session import run
# downloads.cpp built and run with Apple clang.
run("""
clang++ -std=c++23 -O2 -Wall -Wextra downloads.cpp -o downloads && ./downloads
""") Output
$ clang++ -std=c++23 -O2 -Wall -Wextra downloads.cpp -o downloads && ./downloads one after the other: 12960 bytes in 1.0 s with std::async: 12960 bytes in 0.4 s
This output changes from run to run: the times change slightly from run to run
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 downloads_session.py
One after the other, the four took the sum of their times, about a second. With std::async they ran at the same
time, so the whole job took as long as the slowest download, 0.4 s. Each call returned a std::future<long>, kept in
a vector, and the loop over get() collected the results in order: a get() on a finished task returns at once, and
a get() on a running one waits for it. Here is that loop with its four files and their times:
The drawing shows the whole scene; its 8 steps are listed below.
Every step as text
- Two tasks start. At 0 ms, main calls std::async(std::launch::async, download, …) for a.csv and b.csv. Each call starts its task on another thread at once and returns a std::future<long> that main keeps.
- Two more start. c.csv and d.csv start the same way. All four tasks now run at the same time, and main has four futures in a vector.
- get() on a.csv waits. main calls get() on the future of a.csv. The task is still running, so get() waits.
- d and b finish first. d.csv finishes at 100 ms and b.csv at 200 ms. Each task writes its result into its future, which is now ready, while main is still waiting for a.
- a arrives. a.csv finishes at 300 ms, and get() returns its 1,200 bytes.
- b was ready. get() on b.csv returns at once: that task finished 100 ms ago, and its future kept the 3,400 bytes.
- c, the slowest. get() on c.csv waits until 400 ms, when the slowest download finishes with 560 bytes.
- d, at once. get() on d.csv returns its 7,800 bytes at once. The total is 12,960 bytes after 0.4 s, the time of the slowest download, where one after the other took 1.0 s.
Futures make the hand-over safe: the task writes its result once, and the standard guarantees that get() sees it
complete, with no mutex in sight.
Checkpoint
Check: Starting tasks and reading their futures
Exceptions come back through get()
A task can fail, and its exception must not be lost on another thread. std::async catches whatever the task throws,
stores it in the future, and get() throws it again in the thread that asks for the result:
// A task that throws: the future stores the exception, and get() throws it again in the thread that asks.
#include <future>
#include <print>
#include <stdexcept>
#include <string>
int parse_port(const std::string& text) {
const int port = std::stoi(text); // throws std::invalid_argument when text is not a number
if (port < 1 || port > 65535) throw std::out_of_range("port " + text + " is not between 1 and 65535");
return port;
}
int main() {
for (std::string text : {"8080", "eighty", "70000"}) {
std::future<int> port = std::async(std::launch::async, parse_port, text);
try {
std::println("{:>6}: port {}", text, port.get());
} catch (const std::invalid_argument&) {
std::println("{:>6}: get() threw the task's std::invalid_argument", text);
} catch (const std::out_of_range& e) {
std::println("{:>6}: get() threw std::out_of_range: {}", text, e.what());
}
}
} from session import run
# exceptions.cpp with both compilers.
run("""
clang++ -std=c++23 -Wall -Wextra exceptions.cpp -o exceptions && ./exceptions
g++-16 -std=c++23 -Wall -Wextra exceptions.cpp -o exceptions-gcc && ./exceptions-gcc
""") Output
$ clang++ -std=c++23 -Wall -Wextra exceptions.cpp -o exceptions && ./exceptions 8080: port 8080 eighty: get() threw the task's std::invalid_argument 70000: get() threw std::out_of_range: port 70000 is not between 1 and 65535 $ g++-16 -std=c++23 -Wall -Wextra exceptions.cpp -o exceptions-gcc && ./exceptions-gcc 8080: port 8080 eighty: get() threw the task's std::invalid_argument 70000: get() threw std::out_of_range: port 70000 is not between 1 and 65535
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 exceptions_session.py
The trip of the second task’s exception takes five steps, from std::stoi on one thread to the catch that handles
it in main:
The drawing shows the whole scene; its 5 steps are listed below.
Every step as text
- The task starts. main calls std::async(std::launch::async, parse_port, text) with text "eighty", and the task starts on another thread.
- The task throws. std::stoi cannot read a number from "eighty" and throws std::invalid_argument. Nothing in the task catches it.
- std::async catches it. std::async catches whatever the task throws and stores it in the future's shared state, which is now ready, holding an exception instead of a value.
- get() throws it again. main's get() finds the shared state ready and throws the stored exception again, in main, with its type intact.
- An ordinary catch. The catch for std::invalid_argument in main handles it, and the program prints "eighty: get() threw the task's std::invalid_argument". The out_of_range of "70000" travels the same way, with its message.
The std::invalid_argument that std::stoi threw on another thread reached main with its type intact, so an
ordinary catch handled it, and so did the std::out_of_range with its message. That is a real difference from a
std::thread, where an exception that leaves the thread’s function ends the whole program with
std::terminate: The function C++ calls when exception handling cannot go on, for example when no handler matches a throw or an exception tries to leave a noexcept function. By default it calls std::abort, which ends the program with SIGABRT. More in “C++ exceptions: throw, catch and stack unwinding”.
Checkpoint
Check: Exceptions that come back through get()
The launch policy decides where and when the task runs
std::async takes an optional first argument, its launch policy, that says how to run the task:
std::launch::asyncruns it as if on a new thread, at once.std::launch::deferredruns nothing yet: the task runs inside the firstget()orwait(), on the calling thread.- No policy means either of the two, and the library chooses.
// The three ways to call std::async, and where and when each one runs its task.
#include <chrono>
#include <format>
#include <future>
#include <print>
#include <string>
#include <thread>
using namespace std::chrono_literals;
int main() {
const std::thread::id main_id = std::this_thread::get_id();
auto where = [main_id](std::string policy) {
return std::format("{}: ran on {}", policy, std::this_thread::get_id() == main_id ? "the main thread" : "another thread");
};
std::future<std::string> a = std::async(std::launch::async, where, "launch::async");
std::future<std::string> d = std::async(std::launch::deferred, where, "launch::deferred");
std::future<std::string> b = std::async(where, "no policy (async | deferred)");
std::println("the deferred task has not started yet: {}", d.wait_for(0s) == std::future_status::deferred);
std::println("{}", a.get());
std::println("{}", d.get()); // the deferred task runs now, inside get(), on this thread
std::println("{}", b.get());
} from session import run
# policies.cpp with both compilers.
run("""
clang++ -std=c++23 -Wall -Wextra policies.cpp -o policies && ./policies
g++-16 -std=c++23 -Wall -Wextra policies.cpp -o policies-gcc && ./policies-gcc
""") Output
$ clang++ -std=c++23 -Wall -Wextra policies.cpp -o policies && ./policies the deferred task has not started yet: true launch::async: ran on another thread launch::deferred: ran on the main thread no policy (async | deferred): ran on another thread $ g++-16 -std=c++23 -Wall -Wextra policies.cpp -o policies-gcc && ./policies-gcc the deferred task has not started yet: true launch::async: ran on another thread launch::deferred: ran on the main thread no policy (async | deferred): ran on another thread
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 policies_session.py
Both libraries started a thread when no policy was given, but the standard allows either choice, so code that needs
the task to run concurrently should say std::launch::async. A deferred task is a lazily computed value: it is
useful when the result may never be needed, and it is a trap when the code assumes it runs in the background.
| Criterion | std::launch::async | std::launch::deferred | No policy |
|---|---|---|---|
| Where the task runs | As if on a new thread | On the thread that calls get() or wait() | Either, as the library chooses |
| When it starts | At once | Inside the first get() or wait() | Either |
| wait_for(0s) before get() | ready or timeout | deferred | Depends on the choice |
| In this lesson's runs | Another thread, both libraries | The main thread, both libraries | Another thread, both libraries |
| When to choose | A task that must run alongside the caller | A value that may never be needed | Code that does not care where or when |
Checkpoint
Check: Launch policies
Promises and packaged tasks fill futures by hand
std::async decides how the task runs. When you already have a thread, or the result appears in a callback, promises let you fulfil a future yourself:
// A std::promise is the writing end of a future: a thread you start yourself fulfils it, and the future reads it.
#include <chrono>
#include <future>
#include <print>
#include <string>
#include <thread>
#include <utility>
using namespace std::chrono_literals;
int main() {
std::promise<std::string> greeting;
std::future<std::string> result = greeting.get_future(); // the reading end, taken before the promise moves away
std::jthread worker([p = std::move(greeting)]() mutable {
std::this_thread::sleep_for(100ms); // some work
p.set_value("hello from the worker");
});
std::println("main is waiting");
std::println("main got: {}", result.get()); // blocks until set_value() has run
} from session import run
# promise.cpp built and run with Apple clang under ThreadSanitizer.
run("""
clang++ -std=c++23 -g -O1 -fsanitize=thread -Wall -Wextra promise.cpp -o promise && ./promise
""") Output
$ clang++ -std=c++23 -g -O1 -fsanitize=thread -Wall -Wextra promise.cpp -o promise && ./promise main is waiting main got: hello from the worker
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 promise_session.py
Step through the hand-over in promise.cpp: who holds which end, and when the value moves:
The drawing shows the whole scene; its 5 steps are listed below.
Every step as text
- Take the reading end first. main creates std::promise<std::string> greeting and calls get_future() before anything else, while it still holds the promise.
- Hand over the writing end. The promise is moved into the worker's lambda, so the worker now owns the writing end. A promise cannot be copied, only moved.
- main waits in get(). main prints "main is waiting" and calls result.get(). The shared state is not ready yet, so get() blocks.
- The worker fulfils it. After 100 ms of work, the worker calls set_value("hello from the worker"). The value goes into the shared state, which becomes ready, once and for good.
- get() returns the value. get() returns the string, complete, with no mutex in sight: the standard guarantees that the reader sees what the writer stored. main prints "main got: hello from the worker", and ThreadSanitizer reported nothing.
One shared state, a writing end and a reading end
Text description of the diagram
The diagram reads from top to bottom.
- At the top is the writing end, used once. It is one of three things: the task of std::async, which returns or throws; a std::promise, through set_value() or set_exception(); or a std::packaged_task, which runs a function.
- An arrow labelled "stores the result, then marks it ready" leads to the shared state in the middle, which holds a value or an exception and whether it is ready.
- An arrow labelled "get() waits until ready, then returns or throws" leads to the reading end at the bottom: a std::future, whose get() can be called once, or a std::shared_future, whose get() can be called any number of times.
All the writing ends meet the reading ends in a shared state: The object that a future shares with its writing end, a std::async task, a std::promise or a std::packaged_task: it holds one value or one exception, and whether it is ready., which holds one value
or one exception and a “ready” flag. std::promise<T> fulfils it with set_value() or set_exception(),
exactly once. std::packaged_task<R()>
wraps a function so that calling the task runs the function and fulfils the future, which is how a queue of jobs
hands each submitter its own result. On the reading side, a std::future can be read once, and a
std::shared_future any number of times, by any number of threads. The Core Guidelines recommend this style for
returning results from concurrent work (CP.60, CP.61).
Checkpoint
Check: Promises, packaged tasks and shared futures
Practise: tasks, futures, policies and promises
Each of the eleven questions below is about std::async, get(), a launch policy or a promise as the four sections above explain them, and the idea with the fewest right answers so far goes first.
Practice that adapts to you
Practise this lesson
11 questions about 7 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Three future traps, reproduced: blocking destructors, deferred polls, a second get() Pro: opens with a Pro, Premium or Ultimate pass
Futures are simple until they are destroyed, polled or read twice. This part reproduces one trap of each kind, times the first, and shows what libc++ and libstdc++ do when the rules are broken.
In this part:
- The future that waits in its destructor, timed
- Polling a deferred future never ends
- A second get() is undefined, and the libraries disagree
1 diagram · 4 runnable examples (C++, Python) · about 6 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Promises and packaged tasks by hand, with their failure cases Pro: opens with a Pro, Premium or Ultimate pass
A promise must be fulfilled exactly once, and every way of breaking that rule has its own error. This part triggers each one, then builds a worker that hands out results of any type through packaged tasks.
In this part:
- Four ways a promise goes wrong
- A job queue with packaged tasks
- Turning a callback into a future
3 runnable examples (C++, Python) · about 4 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
When std::async is the wrong tool, measured, with an exercise Pro: opens with a Pro, Premium or Ultimate pass
Every std::async call with the async policy starts a thread, and starting a thread costs tens of microseconds or more. This part measures what that does to 10,000 small jobs on both libraries, then asks you to use std::async where it fits: a handful of slow, independent tasks.
In this part:
- 10,000 small jobs, three ways, measured
- Your turn: word counts from slow files
4 runnable examples (C++, Python) · about 4 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Interview questions
-
Fresher to mid · Warm-up
What is the difference between std::launch::async and std::launch::deferred, and what does std::async do without a policy?
std::launch::async runs the task as if on a new thread, starting at once, so it really runs at the same time as the caller. std::launch::deferred runs nothing at first: the task is stored in the future and runs on the calling thread inside the first get() or wait(), which is lazy evaluation, not concurrency; wait_for() on such a future returns std::future_status::deferred. Without a policy, std::async may do either, and the implementation chooses. libc++ and libstdc++ both start a thread, but portable code that needs the task to run concurrently should pass std::launch::async. Whatever the policy, get() returns the task's value or throws its exception.
-
Mid to senior
A loop calls std::async(std::launch::async, upload, file) for 20 files without keeping the returned futures. It takes as long as uploading the files one by one. Explain why, and fix it.
-
Mid to senior
A worker thread fulfils a std::promise with its result. In production the consumer sometimes blocks in get() for ever, and sometimes gets std::future_error with broken_promise. What went wrong in each case, and how do you make sure the consumer always receives a value or an error?
-
Mid to senior
You must run 10,000 independent tasks of about 50 microseconds each and combine their results. Would you start each one with std::async? Defend your answer with numbers.
Key takeaways
- std::async runs a function as a task and returns a std::future;
get()waits and returns the value or throws the task’s exception. - Say
std::launch::asyncwhen the task must run concurrently; a deferred task runs inside the firstget(). - A std::promise fulfils a future from code you run yourself; std::packaged_task turns a function into a task with a future.
- A std::future can be read once; use std::shared_future when several readers need the result.
Terms in this lesson
- launch policy
- The first argument of std::async: std::launch::async runs the task as if on a new thread, at once; std::launch::deferred runs it on the caller, inside the first get() or wait(); without one, the library chooses.
- shared state
- The object that a future shares with its writing end, a std::async task, a std::promise or a std::packaged_task: it holds one value or one exception, and whether it is ready.
- std::future
- The reading end of a result that another thread or a deferred task produces: get() waits until the result is ready, then returns the value or throws the stored exception. A std::future can be read once.
- std::promise
- The writing end of a shared state that your own code fulfils, once, with set_value() or set_exception(); get_future() gives the std::future that reads it.
References
- Working Draft, Programming Languages: C++ (N4950), 33.10 Futures (ISO/IEC JTC1/SC22/WG21)
- Async() future destructors must wait (N3679) (ISO/IEC JTC1/SC22/WG21)
- C++ Core Guidelines: CP, Concurrency and parallelism (Standard C++ Foundation)
- UndefinedBehaviorSanitizer (Clang documentation) (LLVM Project)
Report a problem with this lesson
Kept only in this browser. Your Learn progress