The Runtime Theory
C++

C++ Concurrency: Threads and Atomics

How std::thread, std::mutex, and std::atomic work, and the race conditions and memory ordering issues they prevent.

The Runtime Theory Team1 min read
▸ On this page

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:

cpp
#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:

cpp
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 200000

Mutexes

std::mutex provides mutual exclusion:

cpp
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

LockBehavior
std::lock_guardLocks on construction, unlocks on destruction. Cannot be manually unlocked.
std::unique_lockMore flexible: can unlock/relock, can defer locking, works with condition variables.
std::shared_mutexMultiple readers or one writer (C++17).

Condition Variables

std::condition_variable lets threads wait for a condition:

cpp
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:

cpp
std::atomic<int> counter{0};
 
void increment() {
  for (int i = 0; i < 100000; i++) {
    counter.fetch_add(1, std::memory_order_relaxed);
  }
}

Memory Ordering

OrderGuaranteesUse when
memory_order_relaxedAtomicity only, no orderingCounters, statistics
memory_order_acquireReads/writes after this stay afterAcquiring a resource
memory_order_releaseWrites before this stay beforeReleasing a resource
memory_order_acq_relBoth acquire and releaseRead-modify-write operations
memory_order_seq_cstTotal ordering across all threadsDefault — when in doubt

Best Practices

  1. Prefer std::async over manual std::thread for simple tasks.
  2. Prefer std::mutex + std::lock_guard over manual lock/unlock.
  3. Use std::atomic only for simple counters/flags — complex logic should use mutexes.
  4. Avoid shared state — prefer message passing (channels) over shared memory.
  5. Never hold a lock while waiting on I/O.

Not started

Sign in to save your learning progress.

Sign in to save