Async, Futures, and Atomics

This section documents C++23 (ISO/IEC 14882:2024), as published by ISO/IEC JTC1/SC22/WG21 (wg21), verified against the freely available working draft N5046 (eel.is/c++draft) and cppreference.com.

This content was generated with the assistance of AI and should be verified against the working draft and cppreference.com before being relied on in production.

This section’s bibliography lists the reference material consulted while preparing these pages.

std::promise, std::future, and std::shared_future

A promise/future pair moves a value (or an exception) from a producer to a consumer, one time, possibly across threads:

#include <future>
#include <thread>
#include <iostream>

void produce(std::promise<int> p) {
    p.set_value(42);                    // or p.set_exception(std::current_exception()) on failure
}

int main() {
    std::promise<int> p;
    std::future<int> f = p.get_future();
    std::thread t(produce, std::move(p));
    std::cout << f.get() << '\n';         // blocks until produce() calls set_value; re-throws if
    t.join();                              // set_exception was called instead
}

std::future::get() can only be called once; std::shared_future (obtained via future::share()) may be copied and its .get() called from multiple consumers, each receiving the same result.

std::async and Launch Policies

std::async runs a callable and returns a future for its result — often simpler than manually wiring a thread + promise:

#include <future>
#include <iostream>

int compute() { return 6 * 7; }

int main() {
    auto f1 = std::async(std::launch::async, compute);      // runs on a new thread, guaranteed
    auto f2 = std::async(std::launch::deferred, compute);    // runs lazily, on THIS thread, only when .get()
                                                                // or .wait() is actually called
    auto f3 = std::async(compute);                             // implementation-defined choice of the two above

    std::cout << f1.get() << ' ' << f2.get() << '\n';
}

Calling std::async without an explicit policy leaves the choice of whether/when a new thread is spawned up to the implementation — specify std::launch::async explicitly whenever concurrent execution is actually required.

std::packaged_task

Wraps a callable so its result is delivered through a future, decoupling invocation from result retrieval — the building block behind simple thread-pool implementations:

#include <future>
#include <thread>
#include <iostream>

int main() {
    std::packaged_task<int()> task([] { return 42; });
    std::future<int> f = task.get_future();

    std::thread t(std::move(task));      // the task can be handed to any executor -- a thread, a queue, ...
    std::cout << f.get() << '\n';
    t.join();
}

std::atomic and Memory Orders

std::atomic<T> provides lock-free (for most T on most platforms) read-modify-write operations without a mutex:

#include <atomic>
#include <thread>
#include <vector>
#include <iostream>

std::atomic<int> counter{0};

void incrementMany() {
    for (int i = 0; i < 1000; ++i) counter.fetch_add(1, std::memory_order_relaxed);
}

int main() {
    std::vector<std::thread> threads;
    for (int i = 0; i < 4; ++i) threads.emplace_back(incrementMany);
    for (auto& t : threads) t.join();
    std::cout << counter.load() << '\n';   // 4000, no data race
}

The default std::memory_order_seq_cst (used implicitly by load()/store()/fetch_add() with no explicit order) is the safest, simplest choice; std::memory_order_relaxed (only ordering guarantee: atomicity itself) is a deliberate, measured optimization for counters like the one above where ordering relative to other memory operations genuinely does not matter — reach for it only after profiling shows the default order matters, never as a first choice.

std::atomic_ref and atomic<shared_ptr>

std::atomic_ref<T> (C++20) provides atomic operations on an existing, non-atomic object — useful when a type can’t be declared std::atomic<T> throughout (e.g. a field in a struct shared with non-atomic code, or a plain array element updated concurrently just for this one algorithm):

#include <atomic>

struct Stats { int hits = 0; };

void recordHit(Stats& stats) {
    std::atomic_ref<int> atomicHits(stats.hits);   // "stats.hits" itself is a plain int
    atomicHits.fetch_add(1, std::memory_order_relaxed);
}

C++20 also specializes std::atomic<std::shared_ptr<T>>, giving lock-free-when-possible atomic operations on a shared_ptr itself (its pointer/control-block pair) — previously requiring an external mutex around every shared_ptr access from multiple threads.

this specialization is standard-conformant C++20/23 (verified against cppreference and the working draft), but this environment’s libstdc++ 13 does not yet implement it — std::atomic<std::shared_ptr<T>> fails its own is_trivially_copyable static assertion here. A newer libstdc++, or libc++/MSVC, provides it.

Parallel Algorithms with Execution Policies

Many <algorithm>/<numeric> functions accept an execution policy as their first argument, letting the implementation parallelize (and/or vectorize) the operation across cores:

#include <execution>
#include <algorithm>
#include <numeric>
#include <vector>

int main() {
    std::vector<int> v(1'000'000);
    std::iota(v.begin(), v.end(), 0);

    std::sort(std::execution::par, v.begin(), v.end());                       // may run on multiple threads
    long long sum = std::reduce(std::execution::par_unseq, v.begin(), v.end(), 0LL);  // parallel + vectorized
    (void)sum;
}

std::execution::seq (sequential, the default with no policy given), par (parallel, but each element still processed by one thread at a time — no data races assumed between elements), and par_unseq (parallel and vectorized — the callable must itself be safe to interleave/reorder at the instruction level, e.g. no locks inside it) are the three standard policies.

Parallel Map/Fold

std::transform/std::reduce (or std::transform_reduce to fuse both into one pass) with an execution policy are the standard library’s "parallel map/fold":

#include <execution>
#include <numeric>
#include <vector>

int main() {
    std::vector<int> input(100, 3);
    long long sumOfSquares = std::transform_reduce(
        std::execution::par,
        input.begin(), input.end(),
        0LL,
        std::plus<>{},                       // the "fold"/reduce step
        [](int x) { return static_cast<long long>(x) * x; }   // the "map"/transform step
    );
    (void)sumOfSquares;
}

See Also