🚀 C++ Concurrency: Atomics and Async
The Kitchen of a Busy Restaurant 🍳
Imagine a super busy restaurant kitchen. Multiple chefs work at the same time. They all need to share ingredients, update the order board, and coordinate who does what. If they don’t coordinate well, chaos happens—two chefs grab the same tomato, or orders get mixed up!
C++ concurrency is like being the head chef who organizes everything so meals come out perfectly, even with many cooks working together.
🧩 What We’ll Learn
graph TD A["Concurrency in C++"] --> B["Atomic Operations"] A --> C["Futures & Promises"] A --> D["async Function"] A --> E["std::jthread"] B --> B1["Safe counting"] C --> C1["Send results later"] D --> D1["Easy background tasks"] E --> E1["Smart threads"]
1️⃣ Atomic Operations
The Shared Whiteboard Problem
Picture a whiteboard where chefs write how many pizzas are ready. If two chefs try to update it at the exact same moment, the number gets messed up!
Atomic operations are like having a magic marker that only ONE person can hold at a time. When you use it, your update is complete and correct—no mixing up.
What “Atomic” Means
The word atomic means “indivisible”—like an atom (the smallest piece). An atomic operation happens all at once or not at all. No half-done work!
Simple Example
Without atomic (BAD):
int counter = 0;
// Thread 1
counter++; // Read, add, write
// Thread 2
counter++; // Read, add, write
// Result might be 1 instead of 2!
With atomic (GOOD):
#include <atomic>
std::atomic<int> counter{0};
// Thread 1
counter++; // One complete action
// Thread 2
counter++; // One complete action
// Result is always 2!
Key Atomic Operations
| Operation | What It Does | Example |
|---|---|---|
load() |
Read the value safely | int x = counter.load(); |
store() |
Write a new value safely | counter.store(5); |
++ / -- |
Add or subtract one | counter++; |
fetch_add() |
Add and return old value | counter.fetch_add(3); |
exchange() |
Swap values | counter.exchange(10); |
When to Use Atomics
✅ Simple counters shared between threads ✅ Flags (true/false signals) ✅ Small data that many threads read/write
❌ NOT for complex objects ❌ NOT for data that needs multiple steps
2️⃣ Futures and Promises
The Restaurant Order Ticket 🎫
When you order food at a restaurant, you get a ticket. The ticket is a promise that your food will come. You can go sit down and wait. When the food is ready, you use your ticket to get it.
In C++:
- Promise = The kitchen’s commitment to make your food
- Future = Your ticket to pick up the result
How They Work Together
graph LR A["Promise"] -->|"set_value#40;#41;"| B["Shared State"] B -->|"get#40;#41;"| C["Future"] style A fill:#ff6b6b style B fill:#ffd93d style C fill:#6bcb77
Simple Example
#include <future>
#include <iostream>
int main() {
// Create a promise (kitchen)
std::promise<int> myPromise;
// Get the future (your ticket)
std::future<int> myFuture =
myPromise.get_future();
// Kitchen prepares the result
myPromise.set_value(42);
// You collect your order
int result = myFuture.get();
std::cout << result; // Prints: 42
}
Using with Threads
#include <future>
#include <thread>
void cookPizza(std::promise<std::string> p) {
// Chef works hard...
p.set_value("Pepperoni Pizza Ready!");
}
int main() {
std::promise<std::string> orderPromise;
std::future<std::string> orderTicket =
orderPromise.get_future();
// Start the chef working
std::thread chef(cookPizza,
std::move(orderPromise));
// Do other things while waiting...
// Pick up your order (waits if not ready)
std::string food = orderTicket.get();
chef.join();
}
Important Rules
| Rule | Why |
|---|---|
get() only once |
The ticket is used up |
get() blocks |
Waits until result is ready |
| Move the promise | Can’t copy, must move |
3️⃣ The async Function
The Easiest Way to Cook in Background! 👨🍳
Remember all that promise/future setup? What if someone did it all for you? That’s std::async!
It’s like telling an assistant: “Go make this happen, give me a ticket to get the result later.”
Super Simple Example
#include <future>
#include <iostream>
int bakeACake() {
// Baking takes time...
return 42; // Returns delicious cake
}
int main() {
// Start baking in background
std::future<int> cake =
std::async(bakeACake);
// Do other things while baking...
std::cout << "Preparing plates...\n";
// Get the finished cake
int result = cake.get();
std::cout << "Cake: " << result;
}
Launch Policies
You can tell async HOW to run the task:
| Policy | What It Means |
|---|---|
std::launch::async |
MUST run in new thread |
std::launch::deferred |
Run when you call get() |
| Default | System decides |
// Force new thread
auto f1 = std::async(
std::launch::async,
bakeACake
);
// Run later when needed
auto f2 = std::async(
std::launch::deferred,
bakeACake
);
Passing Arguments
int add(int a, int b) {
return a + b;
}
int main() {
auto result = std::async(add, 10, 20);
std::cout << result.get(); // Prints: 30
}
Why async is Awesome
✅ No manual thread management ✅ No manual promise/future setup ✅ Clean, simple code ✅ Exception handling built-in
4️⃣ std::jthread (C++20)
The Self-Cleaning Chef 🧹
Regular threads (std::thread) are like chefs who leave dirty dishes. YOU must clean up (call join() or detach()).
std::jthread is like a chef who automatically cleans up when they leave! Plus, you can politely ask them to stop cooking.
The “j” Means “Joining”
#include <thread>
void regularThread() {
std::thread t([]{ /* work */ });
// MUST call t.join() or t.detach()
// Or program crashes!
t.join(); // Don't forget!
}
void smartThread() {
std::jthread jt([]{ /* work */ });
// Automatically joins when jt
// goes out of scope!
// No cleanup needed!
}
Stop Tokens: Polite Cancellation
The coolest feature! You can ask a jthread to stop nicely.
#include <thread>
#include <iostream>
void worker(std::stop_token token) {
while (!token.stop_requested()) {
std::cout << "Working...\n";
// Do some work
}
std::cout << "Stopping politely!\n";
}
int main() {
std::jthread chef(worker);
// Let chef work for a bit...
std::this_thread::sleep_for(
std::chrono::seconds(2)
);
// Ask chef to stop
chef.request_stop();
// jthread auto-joins here!
}
Comparing thread vs jthread
| Feature | std::thread |
std::jthread |
|---|---|---|
| Auto-join | ❌ No | ✅ Yes |
| Stop token | ❌ No | ✅ Yes |
| Forget to join | 💥 Crash | ✅ Safe |
| C++ version | C++11 | C++20 |
Stop Callback
Want something to happen when stop is requested?
void worker(std::stop_token token) {
std::stop_callback cb(token, []{
std::cout << "Cleanup time!\n";
});
while (!token.stop_requested()) {
// Work...
}
}
🎯 Quick Summary
graph TD A["Need shared<br>counter?"] -->|Yes| B["Use Atomic"] A -->|No| C{Need result<br>from thread?} C -->|Yes| D{Want easy<br>setup?} D -->|Yes| E["Use async"] D -->|No| F["Use Promise<br>+ Future"] C -->|No| G{Need stop<br>support?} G -->|Yes| H["Use jthread"] G -->|No| I["Use thread"]
🏆 Key Takeaways
- Atomic = Magic lock-free updates for simple values
- Promise + Future = Send results between threads
- async = Easy background tasks with automatic setup
- jthread = Smart threads that clean up and can be cancelled
🎪 Final Analogy Recap
| Concept | Restaurant Analogy |
|---|---|
| Atomic | Magic marker only one can hold |
| Promise | Kitchen’s commitment |
| Future | Your order ticket |
| async | “Just handle it for me” |
| jthread | Self-cleaning chef |
You’re now ready to coordinate your C++ kitchen like a pro head chef! 🧑🍳✨
