The idea: wait on the CPU, or wait in the kernel
When a thread finds a lock taken, it has two ways to wait. A spinlock keeps trying on its own CPU until the lock is free. A mutex asks the kernel to put the thread to sleep, and the thread that unlocks asks the kernel to wake it. This page runs the same two threads through both locks at once: T1 on CPU 0 takes the lock at t = 0, T2 on CPU 1 wants it 10 ns later, and both hold it for the same time. Each Step shows the next event of each lane.
The cost of waiting is the whole point. Spinning costs exactly as long as the wait. Sleeping costs a fixed amount, a few microseconds for two system calls and two context switches, however long the wait is. So the hold time decides which lock wins.
Spinlock: spin until it is free
The lock is one word. xchg(&lock, 1) writes 1 and returns the old value in one atomic step: old 0 means the thread now owns the lock; old 1 means someone else does, so try again. Release is a plain store of 0. Nothing ever enters the kernel. The waiter takes the lock the very moment it is released, and nobody else can use its CPU while it waits (Demo: short hold, 50 ns: T2 spins 40 ns and is in).
Mutex: sleep on a futex
A Linux mutex is a word m plus a kernel wait queue (a futex). m is 0 (free), 1 (locked) or 2 (locked, someone may sleep). Locking an unlocked mutex is one cmpxchg(m, 0, 1), with no system call. Otherwise the thread sets m to 2 and calls futex_wait(&m, 2); the kernel puts it to sleep only if m is still 2. Unlock does xchg(m, 0), and if the old value was 2 it calls futex_wake(&m, 1), which moves one sleeper off the queue. The sleeper then needs a context switch to run again.
With a 50 ns hold this all costs far more than the wait. In Demo: short hold, 50 ns T1 unlocks before the kernel even checks m, so futex_wait returns EAGAIN, and both unlocks call futex_wake because m was 2: 1.4 µs of kernel time to save 40 ns of spinning. With a 5 ms hold (Demo: long hold, 5 ms) the same 3 µs buys 5 ms of free CPU.
How futex_wait and futex_wake find each other in the kernel's hash buckets is animated in How a Mutex Works: the Futex.
When the holder is preempted
A spinner assumes the holder is running and will be done soon. In user space nothing guarantees that: a timer interrupt can take the holder's CPU for a whole time slice. In Demo: holder preempted while T2 waits T1 is preempted inside a 50 ns critical section for 4 ms, and T2 spins for all of it. The mutex lane does not care: T2 is asleep. That is why spinlocks in the Linux kernel turn off preemption while they are held, and why user-space code rarely uses a plain spinlock.
Adaptive mutex: spin a little, then sleep
An adaptive mutex spins first, but only for a short time and only while the owner is running on a CPU; then it sleeps. Short holds end during the spin, with no system call (Demo: adaptive mutex, short hold: a tie with the spinlock, 40 ns). Long holds cost the spin budget plus the normal sleep (Demo: adaptive mutex, long hold: 2 µs + 3 µs). A preempted owner stops the spinning at once (Demo: adaptive mutex, holder preempted). The Linux kernel's own mutex spins this way while the owner runs, glibc has PTHREAD_MUTEX_ADAPTIVE_NP, and Java and Go locks spin briefly before they park.
When to pick which
| Spinlock | Mutex (futex) | |
|---|---|---|
| A waiter | spins on its CPU | sleeps; its CPU runs other threads |
| No contention | one atomic instruction | one atomic instruction |
| Cost of a contended wait | the whole wait, in CPU time | about 3 µs: two system calls, two switches |
| Hand-over latency | immediate | a wake-up, a few µs |
| Holder preempted | waiters spin for the whole time slice | no harm |
| Pick it for | a few instructions; kernel code that must not sleep (interrupt handlers) | almost everything in user space; anything that may block inside |
The scoreboard counts CPU burned waiting (spinning plus kernel time of both threads), how long T2 waited for the lock, and how much CPU time was free for other threads while T2 slept. The winner is the lane that burned less.
Common mistakes
- "Spinlocks are faster." Only when the wait is shorter than a sleep and a wake, and only when the holder is sure to be running.
- A spinlock on a single core. The holder cannot run while the waiter spins, so every spin lasts until the next time slice.
- Sleeping, or doing I/O, while holding a spinlock. Every waiter spins for the whole sleep.
- Counting only the uncontended case. Both locks cost one atomic instruction when nobody waits; they differ only under contention.
See also Spinlock and Mutex, which animate each lock tick by tick with more threads, and Linux Context Switch for what the mutex's sleep and wake cost.
What the page leaves out
- Cache effects: moving the lock's cache line between cores (about 50–100 ns), and spinners that hammer it. Real spinlocks spin on a plain read first (test-and-test-and-set), use
pause, and queue up (ticket and MCS locks) to be fair. - More than two threads, fairness and barging: a woken mutex waiter may lose the lock to a newcomer.
- Real costs vary: system calls, wake-ups and context switches take from under a microsecond to tens of microseconds depending on the CPU, mitigations and caches; the page uses round numbers.
- Priority inversion and priority-inheritance mutexes (
FUTEX_LOCK_PI), reader-writer locks, and lock-free code that avoids both.