Expert · Exceptions, concurrency and building projects with CMake · topic 36 of 42 · 30 min
Concurrency 2: atomics, async, futures and condition variables
Explanation
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.
#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
}std::async and std::future: results from another thread
#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
}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
#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
}
}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?
| Need | Tool |
|---|---|
| A shared counter or a stop flag | std::atomic |
| Several values that must change together | std::mutex + std::lock_guard / std::scoped_lock |
| Run a task and collect its result later | std::async + std::future |
| A thread must sleep until another signals | std::condition_variable |
| Many readers, few writers | std::shared_mutex with std::shared_lock (C++17) |
| Ask a std::jthread to stop | std::stop_token (C++20) |
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
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.
#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
}Practice (0/4 done)
Which is the simplest correct way to share a hit counter between threads?
auto f = std::async(std::launch::async, []{ throw std::runtime_error("x"); return 1; }); What happens at f.get()?
Why should cv.wait be given a condition (a predicate)?
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
Read more (free)
Related help
Before this, learn
This topic builds on these. If something here feels new, open them first.
- Lambdas: small functions written in placeModern C++: lambdas, C++17 and C++20 essentials
- std::optional, std::variant and std::string_viewModern C++: lambdas, C++17 and C++20 essentials
- Concurrency 1: threads, mutexes and data racesExceptions, concurrency and building projects with CMake · the topic just before this one
Related topics
- Building real projects: CMake, warnings, debuggers and sanitizersExceptions, concurrency and building projects with CMake · same module
- Exceptions: throw, try, catch and noexceptExceptions, concurrency and building projects with CMake · same module
- Futures and async/awaitFlutter and Dart · Async Dart and packages
- Async Rust with tokioRust Programming · Concurrency and async
- ThreadsRust Programming · Concurrency and async
- Mutex, Once, atomic and the race detectorGo Programming (backend and cloud) · Concurrency: goroutines, channels and context
Questions people ask about this
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
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.
What does an async fn return when called?Related
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 more topic in “Exceptions, concurrency and building projects with CMake” before its exam: Building real projects: CMake, warnings, debuggers and sanitizers.
- Module 9 exam: Exceptions, concurrency and CMake · 12 questions · 25 min, without help. 4 topics of this module still to finish first.
- Then the next module, Competitive programming, interviews and careers. 42 topics left in the course before the final exam.