Spinlock
- title
- Spinlock
- type
- concept
- summary
- Locking by busy-waiting in a tight loop instead of sleeping; cheap when critical sections are short, catastrophic when they aren't
- tags
- concurrency, kernel, performance
- sources
- linux-broke-postgresql
- created
- 2026-04-30
- updated
- 2026-04-30
A spinlock is the simplest mutual-exclusion primitive there is:
while (!try_acquire_lock()) {
/* keep checking */
}
The waiter doesn't go to sleep. It burns CPU on a tight loop until the lock becomes available.
When this is the right choice
Putting a thread to sleep and waking it back up costs hundreds of nanoseconds to microseconds. If the critical section is shorter than that, sleeping is more expensive than spinning. Short, fast, predictable critical sections โ a few pointer updates, an atomic counter โ are exactly what spinlocks are for.
The whole design rests on one assumption: the holder will release the lock very soon. Nobody is going to preempt a thread in the middle of a 20-nanosecond section. The holder finishes before anyone notices.
When the assumption breaks
Anything that turns a "20 nanosecond" section into something longer ruins the math:
- A page fault inside the critical section. The kernel has to allocate a physical page and update the mapping โ microseconds, not nanoseconds.
- A syscall on the slow path.
- Preemption by the scheduler, especially while the holder is mid-fault. See linux-preemption-models.
When this happens, the cost isn't "one thread waited longer." It's t ร N, where N is the number of waiters spinning. On a 96-vCPU machine with hundreds of contended backends, this multiplier melts CPU.
linux-7-postgres-regression is the canonical instance: PostgreSQL's StrategyGetBuffer holds a global spinlock while touching shared memory that may not be mapped yet. Under PREEMPT_NONE the holder finished the fault and released. Under PREEMPT_LAZY the scheduler can preempt mid-fault, multiplying the spin time by every waiting backend.
Mitigations
- Make the critical section actually short โ no syscalls, no first-touch memory accesses.
- Pre-fault and pre-map memory the section will touch (e.g., huge-pages reduce the number of first-touch faults by orders of magnitude).
- Use Restartable Sequences (
rseq) so the kernel signals preemption and userspace can restart the section. Linux-specific. - Fall back to a sleeping mutex when contention or section length grows beyond what spinning is good for.