Operating Systems · Module 4 — Synchronisation
Mutexes, spinlocks, and test-and-set
Software alone cannot fix this, because any software check is itself several instructions. The CPU has to provide one instruction that does the read and the write together, with no gap.
Sign in to track your score
The obvious way to build an entry section:
if (lock == 0) { lock = 1; } — // go in
Read the lock. If it is free, take it.
But look at what that is. A read, then a write, on a shared variable.
That is exactly the pattern from Topic 4.1. Two threads can both read 0, both decide the lock is free, and both walk in.
We tried to guard a shared thing with a shared thing.
Why & what
The way out is hardware. Software alone cannot fix this, because any software check is itself several instructions. The CPU has to provide one instruction that does the read and the write together, with no gap.
Atomic means exactly that: all or nothing. The scheduler cannot interrupt partway through, and on a multi-core machine no other core can slip in either.
test-and-set. One atomic instruction that does two things:
- Returns the lock's old value.
- Sets the lock to 1.
If the old value comes back as 0, the lock was free and you now own it. If it comes back as 1, someone else already had it. Two threads calling it at the same instant produce one 0 and one 1. Always.
Building a lock from it:
- lock() — call test_and_set until it returns 0.
- unlock() — set the lock back to 0.
Two ways to wait. Once you have a lock, the interesting question is what a thread does while it cannot get in.
- A spinlock loops and keeps checking. It keeps its core the whole time and burns CPU doing nothing useful.
- A blocking mutex puts the thread into the Waiting state from Topic 2.2 and gives the core to somebody else.
Neither is better in general. It is arithmetic, using the 5 µs from Topic 2.3:
- Expected wait shorter than a context switch → spin. Sleeping and waking would cost more than the wait itself.
- Expected wait longer → block. Burning a core for milliseconds is far worse than paying 5 µs twice.
That is why kernels use spinlocks internally for very short critical sections, and why the locks in your own programs are almost always blocking.
Mutex versus binary semaphore. They look identical from outside. The difference is ownership: a mutex has an owner, and only the thread that locked it may unlock it. A semaphore has no owner and anyone may signal it.
How it works
The browser's network thread updating the counter:
- It calls lock(). test_and_set returns 0, so the lock was free and is now held.
- It enters the critical section and does the read, add, write on bytes_loaded.
- The render thread calls lock() while this is happening. test_and_set returns 1, so it does not get in. It waits — spinning or sleeping, depending on the lock type.
- The network thread calls unlock(), setting the lock back to 0.
- The render thread's next attempt returns 0 and it goes in. The counter ends up at 202.

Common confusion
"A lock physically stops other threads from touching the data." It does not. Nothing prevents a thread from ignoring the lock and writing to bytes_loaded directly. A lock is a convention that every thread agrees to follow. One thread that forgets to lock breaks the whole scheme, and this is one of the most common real-world synchronisation bugs.
"Spinlocks are just badly written locks." They are the right choice for very short waits.
Spinning for 200 ns beats a 5 µs sleep-and-wake by a wide margin. What makes a spinlock a mistake is using it for a long wait — or using one on a single-core machine, where the spinning thread holds the only core and the lock holder cannot possibly run to release it.
"A high-priority thread can never be blocked by a low-priority one." It can, and the effect has a name: priority inversion. Suppose the music player (high priority) needs a lock that the compile job (low priority) already holds. The music player must wait. Worse, if the browser (medium priority) keeps preempting the compile job, the compile job cannot finish and release the lock — so a medium-priority thread ends up blocking a high-priority one. The usual fix is to temporarily raise the lock holder's priority until it releases, which is Module 3's aging idea pointed at a lock.
"Atomic just means fast." It means indivisible. A slow atomic instruction is still atomic. Speed is not the point; the absence of a gap is.
Interview angle
"Why can't you implement a lock with an ordinary if-statement?" Because the check and the set are two instructions with a gap between them, so two threads can both pass the check.
Then name the fix: one atomic hardware instruction such as test-and-set.
"Spinlock or mutex?" Answer with the comparison, not a preference: spin when the expected wait is shorter than a context switch, block when it is longer. Quoting the 5 µs figure makes the answer concrete.
"Difference between a mutex and a binary semaphore?" Ownership. A mutex can only be unlocked by the thread that locked it; a semaphore can be signalled by anyone. This is a favourite short question.
- 1.
Two threads call test_and_set on a free lock at the same instant. What do they get back?
- 2.
A thread expects to wait about 300 ns for a lock. Context switching costs 5 us. Which is better?