latency.lab
Module 10

Concurrency & Multithreading

Level: advancedTime: ~2.5 hrsPrereq: Module 09

By the end you can

You did POSIX locks at Cisco, so the concepts here are familiar — this is the C++ standard-library expression of them. Concurrency is central at trading firms: market data on one thread, order management on another, all touching shared state. Getting it wrong causes the worst kind of bug: the one that only appears under load.

Threads

thread.cpp
#include <thread>

void work(int id) { std::cout << "thread " << id << "\n"; }

std::thread t1(work, 1);   // starts running immediately
std::thread t2(work, 2);
t1.join();                 // wait for t1 to finish
t2.join();                 // wait for t2 — you MUST join (or detach) every thread
Join or detach — always

If a std::thread is destroyed while still joinable (neither joined nor detached), the program calls std::terminate and dies. This is a common interview gotcha. The RAII fix is std::jthread (C++20), which auto-joins in its destructor.

The race condition

When two threads read-modify-write the same data without synchronization, the result is undefined and non-deterministic.

race.cpp
int counter = 0;
// two threads both doing: counter++;  (read, add, write — NOT atomic)
// final value is unpredictable: increments get lost

Mutex + RAII locks

A mutex ensures only one thread enters a critical section at a time. Never lock/unlock by hand — use an RAII lock so it's released even on exceptions (RAII from Module 04, applied to locking).

mutex.cpp
#include <mutex>

std::mutex m;
int counter = 0;

void safe_increment() {
    std::lock_guard<std::mutex> lock(m);  // locks now, unlocks at scope exit
    ++counter;                            // protected critical section
}   // lock released automatically here
  • std::lock_guard — simplest RAII lock: lock on construction, unlock on destruction.
  • std::unique_lock — more flexible: can unlock/relock, and works with condition variables.

Deadlock

Two threads each hold a lock the other needs, and both wait forever.

deadlock.cpp
// Thread A: lock(m1); lock(m2);
// Thread B: lock(m2); lock(m1);   // opposite order — deadlock possible

// FIX 1: always acquire multiple locks in the SAME global order
// FIX 2: use std::scoped_lock to lock several at once, deadlock-free
std::scoped_lock lock(m1, m2);   // C++17: locks both without deadlock

std::atomic

For simple shared values (counters, flags), an atomic gives lock-free thread-safe operations — no mutex needed.

atomic.cpp
#include <atomic>
std::atomic<int> counter{0};
++counter;                    // atomic: no race, no lock
std::atomic<bool> done{false};   // classic stop flag between threads

Condition variables — the producer-consumer pattern

A condition_variable lets a thread sleep until another signals that work is ready — the backbone of a work queue.

prodcons.cpp
std::mutex m;
std::condition_variable cv;
std::queue<Task> q;

void producer(Task t) {
    { std::lock_guard<std::mutex> lk(m); q.push(t); }
    cv.notify_one();                 // wake a waiting consumer
}

void consumer() {
    std::unique_lock<std::mutex> lk(m);
    cv.wait(lk, []{ return !q.empty(); });  // sleeps until predicate true; guards spurious wakeups
    Task t = q.front(); q.pop();
}
Always use the predicate form of wait

cv.wait(lk, predicate) re-checks the condition on wake, protecting against spurious wakeups. The bare cv.wait(lk) without a predicate is a bug waiting to happen. This exact detail shows up in concurrency interviews.

Build

Exercise 10.1 — Thread-safe queue

  • First, reproduce a race: two threads increment a plain int a million times each; observe the total is wrong. Fix it two ways — with a mutex, and with std::atomic<int>.
  • Build a ThreadSafeQueue<T> with push and a blocking pop using mutex + condition_variable (predicate form).
  • Run one producer thread and two consumer threads through it and confirm no items are lost or duplicated.
  • Deliberately create a two-mutex deadlock, confirm it hangs, then fix it with std::scoped_lock.

Takeaways

  • Every std::thread must be joined or detached, or the program terminates.
  • Unsynchronized shared read-modify-write is a race condition — undefined and non-deterministic.
  • Protect critical sections with a mutex via RAII locks (lock_guard/unique_lock), never manual lock/unlock.
  • Prevent deadlock with consistent lock ordering or std::scoped_lock.
  • atomic for simple values; condition_variable (predicate form) for producer-consumer.
Live

Loading starter code…

Checkpoint · Concurrency

6 questions · pass at 70%

Q01
What happens if a joinable std::thread is destroyed without join() or detach()?
WhyDestroying a still-joinable thread calls std::terminate. You must join or detach every thread; std::jthread (C++20) auto-joins.
Q02
Two threads run counter++ on a plain int concurrently. Why is the result unpredictable?
Whycounter++ is three steps (load, add, store). Without synchronization the steps from different threads interleave, losing updates. This is a data race — undefined behavior.
Q03
Why use std::lock_guard instead of manual mutex.lock()/unlock()?
Whylock_guard is RAII for locking: it unlocks in its destructor on every exit path, preventing the classic "returned/threw without unlocking" deadlock.
Q04
How can you acquire two mutexes without risking deadlock?
WhyDeadlock needs inconsistent lock ordering. Enforce a single global order, or use std::scoped_lock (C++17) which locks multiple mutexes atomically without deadlock.
Q05
For a simple shared counter, what avoids a mutex entirely?
Whystd::atomic provides lock-free atomic operations for simple values. (volatile does NOT provide thread safety — a common misconception.)
Q06
Why use the predicate form cv.wait(lk, pred) rather than cv.wait(lk)?
WhyCondition variables can wake spuriously. The predicate form loops until the condition is actually true, which is the correct, robust pattern.

Finished Module 10?

Pass the quiz to complete it automatically.

+ Note