C++ Concurrency: Threads and Atomics
Modern C++ provides standard concurrency primitives: threads, mutexes, condition variables, and atomics.
Threads
std::thread starts a new thread of execution:
#include <thread>
#include <iostream>
void worker(int id) {
std::cout << "Thread " << id << " is working\n";
}
int main() {
std::thread t1(worker, 1);
std::thread t2(worker, 2);
t1.join(); // wait for t1 to finish
t2.join(); // wait for t2 to finish
return 0;
}Race Conditions
When two threads access the same variable and at least one writes, you have a race condition — undefined behavior:
int counter = 0;
void increment() {
for (int i = 0; i < 100000; i++) {
counter++; // ❌ race condition
}
}
// If two threads run this, counter is unlikely to be 200000Mutexes
std::mutex provides mutual exclusion:
std::mutex mtx;
int counter = 0;
void increment() {
for (int i = 0; i < 100000; i++) {
std::lock_guard<std::mutex> lock(mtx);
counter++; // ✅ protected
}
}Lock Types
| Lock | Behavior |
|---|---|
std::lock_guard | Locks on construction, unlocks on destruction. Cannot be manually unlocked. |
std::unique_lock | More flexible: can unlock/relock, can defer locking, works with condition variables. |
std::shared_mutex | Multiple readers or one writer (C++17). |
Condition Variables
std::condition_variable lets threads wait for a condition:
std::mutex mtx;
std::condition_variable cv;
bool ready = false;
// Thread 1 (producer)
{
std::lock_guard<std::mutex> lock(mtx);
ready = true;
cv.notify_one(); // wake up one waiting thread
}
// Thread 2 (consumer)
{
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, []{ return ready; }); // wait until ready is true
// ready is now true
}Always use the predicate form of wait() to avoid spurious wakeups.
Atomics
std::atomic provides lock-free operations for simple types:
std::atomic<int> counter{0};
void increment() {
for (int i = 0; i < 100000; i++) {
counter.fetch_add(1, std::memory_order_relaxed);
}
}Memory Ordering
| Order | Guarantees | Use when |
|---|---|---|
memory_order_relaxed | Atomicity only, no ordering | Counters, statistics |
memory_order_acquire | Reads/writes after this stay after | Acquiring a resource |
memory_order_release | Writes before this stay before | Releasing a resource |
memory_order_acq_rel | Both acquire and release | Read-modify-write operations |
memory_order_seq_cst | Total ordering across all threads | Default — when in doubt |
Best Practices
- Prefer
std::asyncover manualstd::threadfor simple tasks. - Prefer
std::mutex+std::lock_guardover manual lock/unlock. - Use
std::atomiconly for simple counters/flags — complex logic should use mutexes. - Avoid shared state — prefer message passing (channels) over shared memory.
- Never hold a lock while waiting on I/O.