Skip to content

Expert · Exceptions, concurrency and building projects with CMake · topic 36 of 42 · 30 min

Concurrency 2: atomics, async, futures and condition variables

Explanation

std::atomic<T> makes simple shared values like counters and flags safe without a mutex. std::async runs a function, possibly on another thread, and gives a std::future you can later .get() the result from (exceptions travel too). std::condition_variable lets a thread sleep until another thread signals that something is ready.

Atomics for simple shared values

Locking a mutex to add 1 to a counter works but is heavy. std::atomic<int> makes each operation on the value indivisible ("atomic"): no other thread can see it half-done. Atomics are perfect for counters, flags like "stop now", and statistics.

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

std::atomic<long long> visits{0};

int main() {
    std::vector<std::jthread> ts;
    for (int t = 0; t < 4; ++t) {
        ts.emplace_back([] { for (int i = 0; i < 100000; ++i) ++visits; });
    }
    ts.clear();                         // join all
    std::cout << visits << '\n';        // always 400000
}
++ on an atomic is a single indivisible operation, so no updates are lost.
Careful: Atomics protect ONE variable at a time. If two values must change together (move money from account A to account B), two separate atomics are not enough: another thread could see A changed but B not yet. Use a mutex around both changes.

std::async and std::future: results from another thread

cpp
#include <future>
#include <iostream>
#include <string>

int fetchPrice(const std::string& item) {
    // imagine a slow network call here
    return item == "gold" ? 7200 : 100;
}

int main() {
    std::future<int> gold = std::async(std::launch::async, fetchPrice, "gold");
    std::future<int> silver = std::async(std::launch::async, fetchPrice, "silver");
    // ... do other work here ...
    std::cout << gold.get() + silver.get() << '\n';   // waits if needed, then 7300
}
std::launch::async asks for a new thread. future.get() waits for the result; if the function threw, get() rethrows that exception in the caller.
Think of it like this: A future is like the token you get when you order food at a busy counter. You can sit down and chat (do other work). When you need the food, you go with the token (call get()): if it is ready you take it at once, otherwise you wait at the counter. If the kitchen burnt your order, you hear about it at the counter (the exception).

get() can be called only once on a std::future. Without std::launch::async, the library may choose to run the function lazily on the calling thread when you call get(), so always pass the policy when you want real parallelism.

condition_variable: wait until something is ready

cpp
#include <condition_variable>
#include <mutex>
#include <queue>

std::mutex m;
std::condition_variable cv;
std::queue<int> orders;

void producer() {
    for (int id = 1; id <= 3; ++id) {
        { std::lock_guard<std::mutex> lock(m); orders.push(id); }
        cv.notify_one();                               // wake one waiting consumer
    }
}

void consumer() {
    for (int handled = 0; handled < 3; ++handled) {
        std::unique_lock<std::mutex> lock(m);
        cv.wait(lock, [] { return !orders.empty(); });   // sleeps until the condition is true
        int id = orders.front();
        orders.pop();
        lock.unlock();
        // process order id without holding the lock
    }
}
Always wait with a condition (the lambda): it handles "spurious wakeups" and notifications that arrived before waiting.

This producer–consumer pattern is the core of thread pools, job queues and logging threads. In real projects, prefer a well-tested library queue or thread pool over writing your own.

Which tool when?

NeedTool
A shared counter or a stop flagstd::atomic
Several values that must change togetherstd::mutex + std::lock_guard / std::scoped_lock
Run a task and collect its result laterstd::async + std::future
A thread must sleep until another signalsstd::condition_variable
Many readers, few writersstd::shared_mutex with std::shared_lock (C++17)
Ask a std::jthread to stopstd::stop_token (C++20)
Tip: The C++17 parallel algorithms let you write std::sort(std::execution::par, v.begin(), v.end()); to ask the library to sort using several threads. Support depends on the standard library (with GCC it needs Intel TBB installed), so measure before relying on it.

Need help?

Stuck on a word or an idea? Search this course, or every course, and jump straight to the explanation or a worked question.

Try:

Watch the lesson

Concurrency 2: atomics, async, futures and condition variables · 1/6

Atomics · futures · condition variables

More concurrency tools: atomics, futures and condition variables.

Prefer a video by a teacher? Find free videos on “c++ std::atomic std::async future condition_variable tutorial” on YouTube ↗

In real life: Loading a dashboard from three services

A trading app's home screen needs the user's holdings, today's market indices and news. Each comes from a different service and takes about 200 ms. Calling them one after another takes 600 ms; starting all three with std::async and then calling get() on each takes about 200 ms in total.

cpp
#include <chrono>
#include <future>
#include <iostream>
#include <string>
#include <thread>

std::string callService(const std::string& name) {
    std::this_thread::sleep_for(std::chrono::milliseconds(200));   // pretend network delay
    return name + " ok";
}

int main() {
    auto start = std::chrono::steady_clock::now();
    auto h = std::async(std::launch::async, callService, "holdings");
    auto i = std::async(std::launch::async, callService, "indices");
    auto n = std::async(std::launch::async, callService, "news");
    std::cout << h.get() << ", " << i.get() << ", " << n.get() << '\n';
    auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::steady_clock::now() - start).count();
    std::cout << "took about " << ms << " ms\n";   // about 200, not 600
}
Independent slow calls overlap instead of queueing.

Practice (0/4 done)

Task 1 · Question

Which is the simplest correct way to share a hit counter between threads?

Task 2 · Question

auto f = std::async(std::launch::async, []{ throw std::runtime_error("x"); return 1; }); What happens at f.get()?

Task 3 · Question

Why should cv.wait be given a condition (a predicate)?

Task 4 · Practice

Use std::async to count the prime numbers below 2,00,000 in 4 parts in parallel (0–50,000, 50,000–1,00,000, and so on), each part returning its count through a future. Add the four results and compare the time with a single-threaded loop using <chrono>.

Quick quiz

Question 1

1.How many times can you call get() on one std::future?

Question 2

2.Why pass std::launch::async to std::async?

Question 3

3.Two bank balances must be updated together by several threads. Which tool?

Question 4

4.What does notify_one() on a condition_variable do?

Read more (free)

Open a question to see the answer and why. The first ones are this topic's own quiz, so try the quiz above before you peek.

How many times can you call get() on one std::future?

Answer: Once

After get(), the future no longer holds a value. std::shared_future allows several reads.

Why pass std::launch::async to std::async?

Answer: To require running on a new thread instead of possibly deferring until get()

Without it, the implementation may defer the call.

Two bank balances must be updated together by several threads. Which tool?

Answer: One std::mutex locked around both updates

Only a lock makes the combined change appear all at once.

What does notify_one() on a condition_variable do?

Answer: Wakes one thread waiting on it

notify_all() wakes every waiting thread.

What does an async function return?Related

Answer: A Future

async functions return Futures.

From Futures and async/await

Why use std::lock_guard instead of calling m.lock() and m.unlock() yourself?Related

Answer: It unlocks automatically, even when an exception is thrown (RAII)

Manual unlock is easy to miss on an early return or exception.

From Concurrency 1: threads, mutexes and data races

What does an async fn return when called?Related

Answer: A Future

The body runs only when the future is awaited or spawned.

From Async Rust with tokio

What is special about std::thread::scope?Related

Answer: Threads in it can borrow local data because they must finish inside the scope

Scoped threads are joined before the scope ends.

From Threads

Pass the quiz (60% or more) or mark this topic as done when you are ready.

Your progress is saved on this device only.

Next steps

  1. 1 more topic in “Exceptions, concurrency and building projects with CMake” before its exam: Building real projects: CMake, warnings, debuggers and sanitizers.
  2. Module 9 exam: Exceptions, concurrency and CMake · 12 questions · 25 min, without help. 4 topics of this module still to finish first.
  3. Then the next module, Competitive programming, interviews and careers. 42 topics left in the course before the final exam.

← Previous: Concurrency 1: threads, mutexes and data races