In this post I will explain the fundamentals of concurrency in C++: processes vs threads, memory models & synchronization, and the C++ memory model. I’ll also provide example code you can try.
Processes vs Threads
- Processes
Independent execution units with their own memory space. Context switching is expensive because the OS must update MMU (Memory Management Unit) state. Processes are great for isolation (e.g., Chrome tabs).
- Threads
Multiple execution units within the same process, sharing memory space. Cheaper than processes, but introduce risks like race conditions.
- Example
#include <iostream> #include <thread> void worker(int id) { std::cout << "Hello from thread " << id << "\n"; } int main() { std::thread t1(worker, 1); std::thread t2(worker, 2); t1.join(); t2.join(); }
- Two threads are created:
t1
runsworker(1)
.t2
runsworker(2)
.
- Each thread prints its message:
- Example outputs:
Hello from thread 1 Hello from thread 2
- But the order is not guaranteed. Sometimes thread 2 may print before thread 1, because the OS scheduler decides which thread runs first.
t1.join()
andt2.join()
ensure the main program waits for both threads to finish before exiting.
- Two threads are created:
Memory Models & Synchronization
- Race Condition
Two threads modify shared state at the same time without coordination. Example: two threads incrementing a counter.
- Deadlock
Two threads wait forever on each other’s locks.
- Livelock
Threads keep reacting to each other but make no progress (like two people stepping left/right repeatedly in a hallway).
- False Sharing
Two threads update different variables that live in the same CPU cache line. Causes unnecessary cache invalidations.
- Memory Barriers & Cache Coherence
Modern CPUs reorder instructions. Memory barriers enforce ordering and visibility.
The C++ Memory Model
- volatile
Not for threading. Prevents compiler optimizations but does not prevent CPU reordering or guarantee visibility between threads.
- std::atomic
Ensures safe concurrent access. Provides memory ordering guarantees (relaxed, acquire, release, sequential consistency).
Example Code
Example 1: Race Condition
#include <iostream>
#include <thread>
int counter = 0;
void increment() {
for (int i = 0; i < 1000000; i++)
counter++;
}
int main() {
std::thread t1(increment);
std::thread t2(increment);
t1.join();
t2.join();
std::cout << "Final counter = " << counter << "\n";
}
Example 2: Fix with std::mutex
#include <iostream>
#include <thread>
#include <mutex>
int counter = 0;
std::mutex mtx;
void increment() {
for (int i = 0; i < 1000000; i++) {
std::lock_guard<std::mutex> lock(mtx);
counter++;
}
}
int main() {
std::thread t1(increment);
std::thread t2(increment);
t1.join();
t2.join();
std::cout << "Final counter = " << counter << "\n";
}
Example 3: Use std::atomic
#include <iostream>
#include <thread>
#include <atomic>
std::atomic<int> counter(0);
void increment() {
for (int i = 0; i < 1000000; i++)
counter++;
}
int main() {
std::thread t1(increment);
std::thread t2(increment);
t1.join();
t2.join();
std::cout << "Final counter = " << counter << "\n";
}
Example 4: Relaxed Memory Order
#include <iostream>
#include <thread>
#include <atomic>
std::atomic<int> flag(0);
std::atomic<int> data(0);
void writer() {
data.store(42, std::memory_order_relaxed);
flag.store(1, std::memory_order_relaxed);
}
void reader() {
while (flag.load(std::memory_order_relaxed) == 0) {
// spin
}
std::cout << "Reader sees data = " << data.load(std::memory_order_relaxed) << "\n";
}
int main() {
std::thread t1(writer);
std::thread t2(reader);
t1.join();
t2.join();
}
Example 5: Acquire–Release Fix
#include <iostream>
#include <thread>
#include <atomic>
std::atomic<int> flag(0);
std::atomic<int> data(0);
void writer() {
data.store(42, std::memory_order_relaxed);
flag.store(1, std::memory_order_release); // release
}
void reader() {
while (flag.load(std::memory_order_acquire) == 0) { // acquire
// spin
}
std::cout << "Reader sees data = " << data.load(std::memory_order_relaxed) << "\n";
}
int main() {
std::thread t1(writer);
std::thread t2(reader);
t1.join();
t2.join();
}
Takeaways
- Example 1: Why races are dangerous.
- Example 2: Locks enforce correctness but add overhead.
- Example 3: Atomics provide efficient lock-free safety.
- Example 4: Relaxed atomics do not guarantee ordering.
- Example 5: Acquire/release enforces visibility order correctly.