The idea: wait by trying again and again

A spinlock is a single word in memory: 0 means free, 1 means taken. To take it, a thread runs an atomic exchange: xchg(&lock, 1) writes 1 and returns the old value in one step that no other CPU can split. If the old value was 0, the lock was free and the thread now owns it. If it was 1, someone else owns it, and the thread simply tries again. That loop is spinning. To release, the owner writes 0.

T1 on CPU 0 and T2 on CPU 1 both run xchg on the lock word in the same tick. T1's xchg returns 0, so T1 owns the lock and runs counter++. T2's returns 1, so the lock is taken and T2 loops back to try again: spinning.
xchg writes 1 and returns the old value in one step: whoever sees 0 owns the lock, whoever sees 1 spins and tries again.
while (xchg(&lock, 1) == 1)
    ;               /* spin */
counter++;          /* critical section */
lock = 0;           /* release */

The canvas gives every thread its own CPU and runs them on one clock. In each tick every thread runs one tick of its line. The strip at the bottom shows what each CPU did in each tick. Orange S cells are ticks spent spinning: the CPU is busy, but it does nothing useful.

How to use the animation

Pick a scenario tab. Step Tick runs one tick; Run to End runs until every thread is done. The blue line in the code is the line a thread just ran; the coloured markers ◂ show where each thread is.

Why the exchange must be atomic

If a thread first read the lock and then wrote 1 in a separate step, two threads could both read 0 and both walk in. xchg does the read and the write together: the CPU holds the cache line for the whole instruction. When two threads try in the same tick, one of them goes first and gets 0, the other gets 1 (Two threads collide). The page Test-and-Set shows the broken version step by step.

What spinning costs

While a thread spins, its CPU cannot run anything else. The waste grows with the length of the critical section and with the number of waiting threads. In Long section, 3 threads each thread is inside for 6 ticks; T2 spins 7 ticks and T3 spins 14: 21 wasted ticks to do 18 ticks of real work.

Tick strip for Long section, 3 threads: T1 is inside for 6 ticks; T2 spins 7 ticks then is inside 6; T3 spins 14 ticks then is inside 6. 18 ticks of work and 21 ticks of spinning.
Waiters line up behind the holder and every waiting tick is a busy CPU doing nothing: more spinning than work.
ScenarioTicks insideTicks spinning
No contention60
Two threads collide64
Long section, 3 threads1821
Holder preempted69

Spinning still has one big advantage: there is no system call and no context switch. When the lock is held for a few hundred nanoseconds, waiting in a loop is faster than going to sleep and being woken up. That is the trade-off with a mutex, which puts a waiting thread to sleep.

The holder is preempted

A spinlock assumes the holder is running and will let go soon. If a timer interrupt takes the CPU away from the holder, everyone else spins for the whole time the holder is off the CPU (Holder preempted: T2 spins 9 ticks for a 3-tick critical section). On a machine with one CPU it is worse: the spinner uses up its time slice and the holder cannot run at all.

This is why the Linux kernel's spin_lock() turns off preemption on the CPU until spin_unlock(), and spin_lock_irqsave() also turns off interrupts when an interrupt handler may take the same lock (see Kinds of Interrupts in Linux). User programs cannot turn off preemption, so user-space spinlocks such as pthread_spin_lock are only a good idea when threads are pinned to their own CPUs.

When to use a spinlock

  • The critical section is very short (a few instructions), and
  • the holder cannot be preempted or sleep while holding it (kernel code, interrupt handlers), or every thread has its own CPU.
  • Otherwise use a mutex. Glibc's adaptive mutex and the Linux kernel's mutex spin briefly first and then sleep, getting the best of both.

What the page leaves out

  • Test-and-test-and-set: real spinlocks spin on a plain read and only try xchg when the lock looks free, so the waiters don't keep stealing the cache line from each other. They also run a pause instruction in the loop.
  • Fairness: here T1 always wins a tie. A plain spinlock has no queue, so a thread can lose many times in a row. Ticket locks (take a number, wait for "now serving") and MCS / qspinlock (each waiter spins on its own cache line, used by Linux today) fix that.
  • Memory ordering: the acquire and release barriers that keep the critical section's reads and writes between lock and unlock.
  • Real timing: one tick here stands for anything from a few nanoseconds (one xchg) to a whole critical section.