Why architecture matters here
The decision to spin or sleep is governed by a simple comparison that the architecture must respect: the expected lock hold time versus the cost of a context switch. A context switch is expensive — plausibly on the order of a microsecond once you count the kernel transition, register save/restore, TLB and cache effects, and scheduler bookkeeping. If the critical section is shorter than that, sleeping is absurd: you would spend more time going to sleep and waking up than the lock is even held. Spinning wins because the waiting thread is ready to pounce the nanosecond the lock frees, paying only a few cache-coherence cycles instead of a full round-trip through the kernel. If the critical section is longer than a context switch — it does I/O, takes another lock, or runs a nontrivial computation — the comparison inverts and sleeping wins decisively, because a spinning thread wastes an entire core for the whole duration.
This is why the architecture matters: a spinlock is correct only under a specific and narrow set of conditions, and using it outside them is not a minor inefficiency but a serious pathology. The condition is roughly: the critical section is extremely short, there are multiple physical cores so the spinner and the holder can run simultaneously, and the code inside the lock never blocks. Violate any of these and the spinlock degrades badly. The design's whole value is concentrated in that narrow regime — kernel data structures, lock-free algorithm fallbacks, per-CPU state — where the alternative's context-switch overhead would dominate.
The second load-bearing fact is that spinning is not free even when it is correct: it generates cache-coherence traffic. A naive spinlock where every waiting thread repeatedly writes-attempts the shared flag causes the cache line holding that flag to bounce between cores' caches on every attempt — the 'cache-line ping-pong' that can saturate the interconnect and slow down not just the spinners but the lock holder trying to release. This is why the variants exist: test-and-test-and-set spins on a read (cheap, cache-local) and only attempts the write when the flag looks free; ticket and MCS locks arrange for each waiter to spin on a different location so releasing wakes exactly one, eliminating the ping-pong. The architecture of the spin loop itself, not just the decision to spin, determines whether a spinlock scales to many cores or collapses under its own coherence traffic.
The third reason to understand this deeply is that a spinlock has a fatal interaction with the scheduler: if the thread holding the spinlock is preempted, every thread spinning for it wastes CPU until the holder is rescheduled — and on a single-core system, the spinner literally prevents the holder from ever running, a guaranteed deadlock-by-livelock. This is why kernel spinlocks disable preemption (and often interrupts) while held, and why user-space spinning is dangerous: you do not control when the scheduler preempts your lock holder. The correctness of a spinlock depends on assumptions about scheduling that the primitive itself cannot enforce, which is precisely what makes it an expert's tool.
The architecture: every piece explained
At the center is the lock flag: a single word of memory, accessed only through atomic operations, holding 0 for free and 1 for held. acquire() is the heart of the primitive — an atomic read-modify-write, typically compare-and-swap (CAS) or test-and-set, that attempts to change the flag from 0 to 1 in one indivisible step. If it succeeds, the thread has the lock and enters the critical section, which for a correct spinlock is very short and never blocks. If the CAS fails — someone else holds the lock — the thread enters the spin loop, retrying until the flag flips back to free.
The spin loop is where the real engineering lives. A naive loop hammers CAS in a tight cycle, which is both wasteful and cache-hostile. The refinements: backoff inserts a CPU PAUSE instruction (which hints the core to de-pipeline the spin and saves power and coherence bandwidth) and, under heavy contention, exponentially increasing delays so waiters don't all storm the flag at once. release() stores 0 back to the flag with the appropriate memory barrier, so that every write the holder made inside the critical section is visible to the next acquirer before it sees the lock as free — the acquire/release memory ordering that makes the lock actually protect data, not just the flag.
The bottom row is the family of smarter designs. A plain test-and-set lock suffers cache-line ping-pong; TTAS (test-and-test-and-set) spins on a plain read of the flag and only issues the expensive atomic write when the read shows free, keeping the spin cache-local. A ticket lock hands each arriving thread a monotonically increasing number and serves them in order by comparing against a 'now serving' counter, giving fairness (FIFO) and eliminating starvation. An MCS lock goes further: each waiter spins on its own local flag in a queue node, so releasing the lock signals exactly the next waiter with no global contention — the design that scales to many cores. Finally, adaptive locks recognize that no single strategy is universal: they spin for a bounded number of iterations and then fall back to a mutex (sleep) if the lock is still held, capturing the fast path for short holds and the efficiency of sleeping for long ones. The ops strip names the invariants: a strict hold-time budget, awareness that spinning only makes sense with more cores than runnable threads, and the rule that code inside a spinlock must never do anything that could be preempted or blocked.
It is worth being precise about why the atomic operation must be a single indivisible read-modify-write and not a read followed by a write. If a thread read the flag as free and then, in a separate instruction, wrote 1, two threads could both read free before either writes — and both would believe they hold the lock, corrupting the data the lock exists to protect. Compare-and-swap collapses the read, the comparison, and the write into one operation the hardware guarantees is atomic across cores, so exactly one of the racing threads succeeds and the rest observe the failure and spin. That atomicity, provided by the cache-coherence protocol and the CPU's locked instructions, is the bedrock the entire primitive stands on; everything above it — backoff, fairness, MCS queues — is optimization around that one guaranteed-atomic swap.
End-to-end flow
Trace a contended acquire on a multi-core machine. Thread A holds a spinlock protecting a tiny shared counter; the critical section is three instructions long. Thread B, running on a different physical core, calls acquire(). Its CAS attempt to swap the flag from 0 to 1 fails, because A has it at 1. B does not sleep — sleeping would cost far more than A's three-instruction hold — so it enters the spin loop.
Spinning cache-locally. Because this is a TTAS lock, B does not hammer CAS. It spins reading the flag, and that read hits B's own L1 cache, holding the line in shared state — no coherence traffic, no interconnect pressure, just B watching its local copy. Between reads it executes a PAUSE to relax the pipeline. A, on its core, finishes its three instructions and calls release(), storing 0 to the flag with a release barrier. The coherence protocol invalidates B's cached copy of the line and B's next read sees the flag as free.
Winning the lock. Now B issues the atomic CAS to swap 0 to 1. If B is the only waiter it succeeds immediately and enters the critical section; the release barrier A executed pairs with B's acquire so B sees the counter value A wrote. If a third thread C was also spinning, exactly one of B and C wins the CAS and the loser resumes spinning — with a ticket or MCS lock, fairness would guarantee B (who arrived first) wins, whereas plain TTAS makes no such promise and C could starve B under adversarial timing. The whole exchange cost a handful of cache-coherence transactions and zero kernel involvement — orders of magnitude cheaper than two context switches.
The failure case that proves the rule. Now change one variable: A gets preempted by the OS scheduler while holding the lock — perhaps its time slice expired, or a higher-priority thread arrived on its core. A is no longer running, but the flag still reads 1. B spins, burning 100% of its core, checking a flag that cannot change until A is rescheduled to run release(). On a machine with spare cores this is merely wasteful for a scheduling quantum; on a single-core machine, or when B has higher priority than A and monopolizes the only available core, A can never run to release the lock and the system livelocks — B spinning forever for a lock A can never free. This is the priority-inversion-and-preemption pathology that makes user-space spinlocks perilous and is exactly why kernel spinlocks disable preemption while held and why adaptive user-space locks bound their spin and fall back to sleeping: the fallback is what breaks the livelock when the holder isn't promptly rescheduled.