Locks, Mutexes, Semaphores, & Spinlocks
Master synchronization primitives: Mutex (Mutual Exclusion), Counting Semaphores (Rate limiting), Read-Write Locks (RWLock), and Spinlocks (Busy-waiting).
Synchronization Primitives State Machines & Transitions 🔐
Detailed lifecycle states for Mutex (Mutual Exclusion), Counting Semaphore (Resource Pooling), and Read-Write Lock (Shared vs Exclusive).
01.1. The Spectrum of Synchronization Primitives
Synchronization primitives allow multiple threads or processes to coordinate access to shared hardware, memory, or network resources. Choosing the wrong locking primitive can destroy system scalability, causing massive latency spikes or thread starvation under load.
The four primary synchronization primitives in production systems:
- Mutex (Mutual Exclusion Lock): Grants exclusive, single-thread access to a critical section. If Thread A holds the mutex, any other thread attempting to acquire it is put to sleep by the OS scheduler until Thread A releases it.
- Read-Write Lock (RWLock / Shared-Exclusive Lock): Allows concurrent, unlimited readers to access a resource simultaneously, but requires exclusive single-threaded access for writers.
- Counting Semaphore: Maintains an integer counter representing a pool of available permits (e.g., maximum 50 concurrent database connections). Threads increment permits on release and decrement on acquire, sleeping when the count hits zero.
- Spinlock: A busy-waiting lock where the waiting thread repeatedly executes a CPU loop (
PAUSEinstruction / CAS) without yielding to the OS scheduler. Extremely fast (~10-50ns) if the critical section is ultra-short, but burns 100% CPU on long waits.
02.2. Linux Futex (Fast Userspace Mutex) Architecture
In modern Linux systems, mutexes are implemented using Futexes (Fast Userspace Mutexes).
Historically, acquiring a lock required invoking a kernel system call every single time (~1-2μs). Futexes revolutionize this:
- Uncontended Fast Path (User Space): When Thread A acquires an uncontended lock, it executes a single hardware atomic CAS instruction (
LOCK CMPXCHG) in User Space (~15ns) with zero kernel overhead. - Contended Slow Path (Kernel Space): Only if the CAS fails (another thread already holds the lock) does the thread execute the
futex(FUTEX_WAIT)system call, entering the kernel to sleep on a wait queue. - Unlock: The releasing thread atomically clears the lock flag and calls
futex(FUTEX_WAKE)only if waiting threads are present.
03.3. Read-Write Lock (RWLock) & The Reader Starvation Problem
In read-heavy workloads (e.g., an in-memory configuration registry, DNS cache, or product catalog with a 99:1 Read:Write ratio), standard mutexes serialize all reads unnecessarily.
An RWLock provides two lock modes:
RLock()/RUnlock(): Shared read lock. Hundreds of threads read concurrently.Lock()/Unlock(): Exclusive write lock. Blocks all readers and writers.
The Writer Starvation Vulnerability:
If new readers continuously arrive and acquire shared read locks before existing readers finish, the waiting writer thread may starve indefinitely, never getting an opportunity to update the data.
Solution: Production RWLocks (e.g., pthread_rwlock_t with PTHREAD_RWLOCK_PREFER_WRITER_NP, Go sync.RWMutex) prioritize pending writers by blocking subsequent new readers once a writer enters the queue.
package cache
import "sync"
type ThreadSafeCache struct {
mu sync.RWMutex
items map[string]string
}
func (c *ThreadSafeCache) Get(key string) (string, bool) {
c.mu.RLock() // Multiple goroutines can read concurrently!
defer c.mu.RUnlock()
val, ok := c.items[key]
return val, ok
}
func (c *ThreadSafeCache) Set(key string, val string) {
c.mu.Lock() // Exclusive lock: blocks all readers & writers during write
defer c.mu.Unlock()
c.items[key] = val
}⚖️Architectural Trade-offs & Production Realities
Architectural Advantages
- Mutexes guarantee data consistency and prevent race conditions.
- RWLocks eliminate read contention for read-heavy workloads (90%+ reads).
- Counting semaphores provide natural rate-limiting and connection pool throttling.
- Spinlocks eliminate kernel context-switching latency for ultra-short critical sections (<100ns).
Trade-offs & Constraints
- Locks serialize execution, reducing parallel efficiency on multi-core servers.
- Improper lock acquisition order leads to unrecoverable deadlocks.
- Spinlocks burn 100% CPU core utilization if the lock holder is preempted.
HikariCP (the world's fastest Java DB connection pool) uses counting semaphores and lock-free thread-local lists to lease database connections in nanoseconds without lock contention. Envoy Proxy eliminates mutex contention entirely by assigning 1 thread per core and communicating strictly via lock-free ring buffers.
🎯 Staff+ Engineering Takeaways
- Mutex = Exclusive 1-thread access; Futex optimizes uncontended locks to fast user-space CAS.
- RWLock = Unlimited concurrent readers OR 1 exclusive writer (ideal for 90%+ read ratios).
- Counting Semaphore = Throttles access up to N concurrent permits (ideal for DB pools).
- Spinlock = Busy-waits in a CPU loop; optimal only for critical sections shorter than a context switch (~1μs).
- Never hold any lock during network I/O or database transactions.
Topic Knowledge Assessment 🧠
Step through 2 scenario questions to test your staff-level grasp.
In an in-memory configuration cache where 99% of operations are reads and only 1% are writes, which synchronization primitive provides the highest throughput?
How clear and staff-actionable was this system breakdown?