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