2026-08-18
Linux used ticket spinlocks from 2008 to 2014: each waiter atomically increments a "next ticket" counter, then spins reading a shared "now serving" counter until it matches. Fair, simple, and catastrophic at scale.
The problem is all waiters spin on the same cache line. When the lock holder releases (writes now_serving+1), that store invalidates the line in every waiter's L1. All N cores then re-read it, and the coherence traffic scales as O(N²). On a 4-core system this is invisible. On a 64-core box, releasing a hot lock triggers a storm of snoop invalidations across the ring bus, and lock throughput drops as you add cores.
Qspinlock (merged 2014, x86 default 2015) fixes this by making each waiter spin on its own cache line — a per-CPU node in an implicit MCS queue. The lock word itself is 32 bits, encoding three states:
cmpxchg sets the "locked" byte. Zero overhead — same cost as a ticket lock.mcs_node, atomically splices itself onto the tail (encoded in the top 16 bits as CPU# + context), and spins on node->locked. Release writes only to the next waiter's node.Concrete example: the kernel's tasklist_lock (a rwlock, but same principle) was measured by Red Hat on a 4-socket Haswell (72 cores). Under a fork()-heavy workload, ticket spinlock throughput peaked at 16 cores then declined to 40% of peak at 72 cores. Qspinlock scaled monotonically, hitting 3.2× the peak ticket throughput.
Rule of thumb: if a lock is uncontended, qspinlock costs one cmpxchg (~20 cycles). If it's contended by N cores, release costs one cache-line transfer to the next waiter — independent of N. Compare to ticket locks: release costs N cache-line invalidations, so contention scales as N × latency-to-farthest-core (~150ns × 64 = ~10μs of coherence traffic per release on a big box).
The tail encoding is the clever bit: 14 bits for CPU index, 2 bits for context (task/softirq/hardirq/NMI), lets any code path acquire without dynamic allocation. When you nest an IRQ handler inside a task holding a qspinlock, the IRQ uses a different per-CPU node — no collision.
