Saturday, September 26, 2026

Traditional Locks vs. Lock-Free CAS: Choosing the Right Concurrency Model

TL;DR: Lock-free primitives (CAS) offer near-zero overhead under low contention, but traditional locks (Mutexes) protect your CPU when contention spikes. Here is how to choose between them in high-throughput systems.


The Core Philosophy: Pessimism vs. Optimism

When multiple threads access shared memory, you face a fundamental architectural choice:

  1. Traditional Locks (Mutex / Semaphore): Pessimistic. Assumes conflicts are frequent. Threads lock the critical section before touching memory; contending threads are descheduled to sleep by the OS kernel.
  2. Lock-Free CAS (Compare-And-Swap): Optimistic. Assumes conflicts are rare. Threads compute updates speculatively and apply them via hardware-level atomic instructions (like x86 CMPXCHG), retrying in user-space if another thread modified the value first.

Head-to-Head Comparison

Metric / Dimension Traditional Lock (Mutex / Semaphore) Lock-Free CAS (Atomic Primitives)
Concurrency Model Pessimistic (assumes conflict is likely) Optimistic (assumes conflict is rare)
Thread State on Contention Blocked / Descheduled (OS-level sleep) Spins in user-space retry loop
Low Contention Overhead Moderate (lock acquisition metadata) Negligible (single CPU instruction)
High Contention Overhead Heavy context-switch latency, but bounded CPU usage CPU burning & cache line thrashing (spin-retry loops)
Failure Modes Deadlocks, Priority Inversion ABA problem, Thread Starvation
Typical Use Cases Multi-variable updates, long workflows, I/O operations Metrics counters, sequence IDs, Disruptor ring buffers, non-blocking queues

When Lock-Free CAS Shines (and When It Backfires)

The Win: Sub-Microsecond Throughput

For single-variable state updates (e.g., atomic counters, rate-limit sequence generators, metrics collection) under moderate concurrency, CAS incurs almost zero overhead because it executes in user-space without kernel transitions.

// Fast, non-blocking sequence generation
public long nextId(AtomicLong counter) {
    return counter.incrementAndGet(); // Single atomic CAS loop
}

The Trap: The Contention Cliff

When dozens of threads slam into the same memory address simultaneously, CAS threads spin in a tight retry loop: * Cache Line Bouncing: Invalidation storms flood the CPU cache coherency bus (MESI/MOESI protocol). * CPU Saturation: CPU cores spike to 100% running empty retry cycles, causing tail latencies to explode.


When to Stick with Traditional Locks

Mutexes carry the overhead of kernel transitions, but they offer critical safety guarantees:

  • Compound Invariants: Modifying two related variables atomically (e.g., transferring funds between account A and account B).
  • I/O & Long-Running Critical Sections: If a critical section involves disk access, network calls, or sleep, a lock yields the CPU core to other productive threads instead of burning CPU cycles.
  • Controlled CPU Under Pressure: Under extreme burst traffic, sleeping threads preserve system resources rather than crashing cache subsystems.

Quick Decision Checklist

  • Choose Lock-Free (CAS / Atomics) if:

    • You are updating isolated variables or lightweight non-blocking queues (e.g., LMAX Disruptor).
    • Lock hold duration is measured in nanoseconds.
    • You need to avoid thread parking latency in ultra-low-latency paths.
  • Choose Traditional Locks (Mutex / Semaphore) if:

    • Operations involve multiple variables or multi-step business transactions.
    • Contention is heavily saturated and you need predictable CPU bounds.
    • Work inside the critical section includes blocking operations or I/O.

No comments:

Post a Comment