CS343

Concurrency Problems

A map of what breaks in concurrent programs, and which tools prevent each failure. Most of these come from CS343 Ch 5-11.

Failure modes

The core enemies, organized by what kind of progress they block:

failurestate of the threadroot causetypical fix
Deadlockblocked forevercircular wait on resourcesordering, detection, avoidance
Livelockrunning, state keeps changing, no work donesymmetric retreatsrandomization, arbiter
Starvationready, scheduler skips itrule 5 brokenFIFO queues, bounded overtaking, aging
Busy-wait starvationspinning in CAS retryunlucky thread keeps losingbounded retry or wait-free ops
Race Conditionfine per-threadunsynchronized access, depends on ordermutual exclusion
Data Raceundefined behaviourconcurrent access, one write, no syncatomics, locks
ABA Problemlogic bug in CASA→B→A invisible to CAStagged pointers, SC, SMR
Use-after-free (lock-free)segfaultfreed node still reachable via stale pointerSMR: hazard pointers, epochs, RCU
Priority Inversionhigh-prio blocked on low-prio’s lockpreemption mid-critical-sectionpriority inheritance / ceiling

Deadlock vs livelock vs starvation?

All three are “no progress” failures, but:

  • Deadlock threads are blocked, CPU idle
  • Livelock threads are running, burning CPU, but looping
  • Starvation threads are ready, the scheduler just never picks them

Coordination primitives

From lowest level to highest, each one solves the layer below:

Programming models

How you structure concurrent code, each model trades safety for complexity differently:

modelwrite itperfgotchas
Lock-basedeasymeh under contentiondeadlock, priority inversion, starvation
Lock-freehard (raw CAS)greatABA, memory reclamation, unbounded retry
Wait-freevery hardoften worse than lock-freemostly academic outside single-atomic ops
STMeasy (just atomic{})bookkeeping overheadI/O unsafe, needs language support
Message passingmediumgood, no shared statejust moves the problem to the channel

Lock-based (C++)

std::mutex m;
std::vector<int> v;
void add(int x) {
    std::lock_guard<std::mutex> lock(m);  // RAII, releases on scope exit
    v.push_back(x);
}

Lock-free (C++ stack push via CAS)

void push(Node* n) {
    Node* old;
    do {
        old = head.load();
        n->next = old;
    } while (!head.compare_exchange_weak(old, n));  // retry until nobody stomped us
}

Wait-free (atomic counter)

std::atomic<int> counter{0};
void inc() { counter.fetch_add(1); }  // single instruction, bounded steps

STM (Rust, rust-stm)

atomically(|tx| {
    let v = var.read(tx)?;
    var.write(tx, v + 1)  // runtime retries the whole block on conflict
});

Message passing (Go)

ch := make(chan int, 10)
go func() { ch <- 42 }()   // producer sends
fmt.Println(<-ch)          // consumer receives, no shared memory

Memory ordering issues

Compiler and hardware reorder reads and writes unless you tell them not to:

  • Cache coherence makes each address look sequentially consistent, but across addresses ordering is weaker
  • Memory model specifies what reorderings are legal, C++ gives you relaxed, acquire, release, seq_cst
  • Memory barriers force ordering at specific points
  • Disjoint reordering and eliding are the compiler’s optimizations that assume single-threaded semantics

Quick “which one do I reach for”

  • Shared counter: fetch_add (wait-free)
  • Shared queue: lock-free queue with SMR, or just a mutex if uncontended
  • Multi-word atomic update: STM or a coarse lock, don’t try to hand-roll lock-free
  • Producer/consumer: bounded buffer with semaphores or a channel
  • Read-heavy shared state: RW lock, or RCU if you really need it