Unit 3 · OS & concurrency · Lesson 6 of 11
Locks, condition variables, atomics and memory ordering
Step 1: Learn the ideas
- Critical sections and mutual exclusion
- Spin locks: test-and-set, test-and-test-and-set, ticket locks
- Sleeping locks, futexes and two-phase locks
- Lock granularity and readers-writer locks
- Condition variables and Mesa semantics
- Spurious wakeups and predicate loops
- Semaphores for counting and ordering
- Barriers and other coordination patterns
- Atomic operations and compare-and-swap
- Compiler and hardware reordering
- Happens-before, acquire/release and sequential consistency
- Data races and language memory models
Why it matters
Thread-coordination problems are a standard way to test whether you can reason about all interleavings rather than the one you imagined. Beyond puzzles, every backend with a worker pool, cache, connection pool or batching queue relies on these primitives, and many production bugs come from a missing re-check, a flag read without synchronization, or a lock held across slow I/O. Low-level and systems-research interviews go further and ask what ordering an atomic operation guarantees.
Core ideas
Mutexes give exclusion and visibility. Unlocking a mutex and later locking the same mutex creates a happens-before edge: everything the first thread wrote inside its critical section is visible to the next owner. That is why reading a shared variable "just to check" without the lock is a bug, not an optimization. Keep critical sections short, avoid blocking I/O inside them, and choose granularity deliberately: one coarse lock is simple and correct, while finer locks add parallelism and deadlock risk.
How locks are built. Spin locks loop on an atomic test-and-set or compare-and-swap; ticket locks add FIFO fairness. Spinning wastes CPU when the holder has been descheduled, so practical mutexes spin briefly and then sleep. On Linux, a futex-based mutex handles the uncontended case entirely in user space with one atomic instruction and enters the kernel only to sleep or to wake waiters.
Condition variables are waiting rooms, not state. The condition lives in shared variables protected by a mutex; the condition variable only lets a thread sleep until someone says "re-check". wait atomically releases the mutex and sleeps, then re-acquires it before returning. Always re-check in a loop: POSIX allows spurious wakeups, and under the usual Mesa semantics another thread may consume the state between the signal and your wakeup.
lock(m)
while queue is empty: wait(not_empty, m)
item = pop(queue)
signal(not_full)
unlock(m)
Use signal when any single waiter can make progress and all waiters wait for the same condition; use broadcast when waiters wait for different conditions or you cannot tell which one should run.
Semaphores hold a count: wait decrements it or blocks, post increments it and may wake a waiter. They express mutual exclusion (initial count 1), ordering (initial count 0: one thread posts, another waits) and resource counting, such as free slots in a bounded buffer. They have no owner, so misuse is harder to detect than with mutexes.
Atomics and memory order. Compilers and CPUs reorder memory operations whenever a single thread could not tell the difference: x86 lets a later load pass an earlier store through the store buffer, and ARM reorders more. Two unsynchronized conflicting accesses form a data race, which is undefined behavior in C and C++. Atomics provide atomicity, and their memory order provides visibility:
- Release/acquire: a release store publishes every earlier write; an acquire load that reads the stored value sees them. Write
data, thenready.store(true, release); a reader that seesreadythrough an acquire load can safely readdata. - Sequentially consistent: all such operations appear in one global order consistent with each thread's program order. It is the default for C++ atomics and the semantics of Java volatile, and the easiest to reason about, but it can require extra fences (even x86 needs one for a sequentially consistent store).
- Relaxed: atomicity only, suitable for statistics counters whose value does not guard other data.
Common misconceptions
- "
ifis fine if I only signal when the condition is true." Spurious wakeups and competing consumers make thewhileloop mandatory. - "Reading a shared variable without the lock is safe." It is a data race; the compiler may cache the value and you may see stale or torn data.
- "Atomic variables make my operation atomic." Each operation is atomic; check-then-act across two of them is still a race.
- "
volatilemakes C or C++ code thread-safe." It provides neither atomicity nor inter-thread ordering; Java's volatile is a different, stronger concept. - "x86 is sequentially consistent, so ordering does not matter." x86 allows store-to-load reordering, and the compiler reorders regardless; portable code must follow the language memory model.
- "A semaphore is just a mutex." It has no owner and counts, which suits signaling and resource limits rather than protecting invariants with clear ownership.
How to approach it
Solve each coordination problem twice, once with a mutex and condition variables and once with semaphores, and explain why every wait sits in a loop. Then write the message-passing example with release/acquire and explain what relaxed ordering would allow. Graduate interviews look for correct waiting without busy loops; mid-level ones for clean producer/consumer designs and awareness of contention; senior and low-level ones for memory-order reasoning and lock implementation tradeoffs.
Suggested order
- 1Read OSTEP Ch. 26 and 27 (Concurrency: An Introduction; Interlude: Thread API) to refresh races and the pthread API.
- 2Read OSTEP Ch. 28 (Locks): test-and-set, compare-and-swap, ticket locks, yielding, sleeping queues and two-phase locks.
- 3Read OSTEP Ch. 30 (Condition Variables) and Ch. 31 (Semaphores), plus the pthread_cond_wait(3p) rationale on spurious wakeups.
- 4Solve Print in Order, Print FooBar Alternately and Fizz Buzz Multithreaded, each with two different primitives.
- 5Do the OSTEP Ch. 30 producer/consumer homework and explain every broken variant.
- 6Read Russ Cox’s Hardware Memory Models and Programming Language Memory Models essays, then check the definitions on the cppreference memory_order page.
- 7Do the 6.1810 Multithreading lab; continue with Building H2O and puzzles from The Little Book of Semaphores for depth.
Step 2: Check off what you can do
Tap each one you can genuinely do or explain. Each earns 10 XP.
Step 3: Go deeper
Operating Systems: Three Easy PiecesStart here
University of Wisconsin–Madison (free online book)Section: Ch. 28: Locks
Primary reading on how locks are built from atomic instructions and when to spin versus sleep.
Open(opens in a new tab)Link responded, 5 Oct 2026Operating Systems: Three Easy PiecesStart here
University of Wisconsin–Madison (free online book)Section: Ch. 30: Condition Variables
Producer/consumer done correctly, with the broken variants that motivate while loops and two condition variables; read Ch. 31 on semaphores next.
Open(opens in a new tab)Link responded, 5 Oct 2026Memory Models (essay series)
research!rsc (Russ Cox)Intuition for hardware and language memory models, happens-before and data-race-free programs.
Open(opens in a new tab)Link responded, 5 Oct 2026std::memory_order (cppreference)
cppreference.comPrecise definitions of the memory orders and of synchronizes-with, for checking your explanations.
Open(opens in a new tab)Link responded, 5 Oct 2026The Little Book of Semaphores
Green Tea PressSection: Ch. 3: Basic synchronization patterns
Practice bank of coordination puzzles with hints; attempt each one before reading its solution.
Open(opens in a new tab)Link responded, 5 Oct 2026
Step 4: Practice
Solve each LeetCode concurrency problem twice with different primitives (condition variables, then semaphores or events) and write one sentence per wait explaining why it sits in a loop. Avoid sleep-based or busy-wait solutions even when they pass. Then run the OSTEP condition-variable homework and force each broken variant to fail. For memory ordering, write the message-passing example in C++, Rust or Java with release/acquire and again with relaxed ordering, and explain which outcomes each allows; ARM machines such as Apple silicon can expose reorderings that x86 hides. The xv6 Multithreading lab ties context switching to locks and barriers.
- Print in OrderLeetCode(opens in a new tab)Not attemptedLeetCode
- Print FooBar AlternatelyLeetCode(opens in a new tab)Not attemptedLeetCode
- Fizz Buzz MultithreadedLeetCode(opens in a new tab)Not attemptedLeetCode
- Print Zero Even OddLeetCode(opens in a new tab)Not attemptedLeetCode
- Building H2OLeetCode(opens in a new tab)Not attemptedLeetCode
- OSTEP Ch. 30 homework: correct and broken producer/consumer queuesOpen(opens in a new tab)Not attemptedOSTEP homework
- MIT 6.1810 Lab: Multithreading (Fall 2023 offering)Open(opens in a new tab)Not attemptedMIT 6.1810
- OSTEP Ch. 28 homework: lock implementations in an interleaving simulatorOpen(opens in a new tab)Not attemptedOSTEP homework
- MIT 6.1810 Lab: locks (Fall 2026)Open(opens in a new tab)Not attemptedMIT 6.1810
- MIT 6.1810 xv6 labs (system calls, page tables, traps, copy-on-write, locks, file system)Open(opens in a new tab)Not attemptedMIT 6.1810
- OSTEP Ch. 27 homework: finding races and deadlocks with helgrindOpen(opens in a new tab)Not attemptedOSTEP homework
- OSTEP project: parallel zip (pzip)Open(opens in a new tab)Not attemptedOSTEP projects
- OSTEP project: concurrent web serverOpen(opens in a new tab)Not attemptedOSTEP projects
Record attempts on each problem’s page (+15 XP, +10 more when solved on your own).
Step 5: Test yourself
Answer each one out loud or on paper before you finish the lesson.
- Question 1Why must a condition-variable wait sit inside a while loop?
- Question 2What does unlocking a mutex guarantee to the next thread that locks it?
- Question 3When is a spin lock better than a sleeping lock, and when is it worse?
- Question 4What can go wrong if a flag that guards data is a plain variable instead of an atomic?
- Question 5What is the difference between sequential consistency and acquire/release?
- Question 6Why can a semaphore express ordering between threads in a way a mutex should not?
- Question 7When is relaxed ordering acceptable?
You’re done when you can
- Solve three LeetCode concurrency problems without busy waiting, each in under 25 minutes, and justify every wait loop.
- Implement a bounded blocking queue with a mutex and two condition variables from memory, and explain what goes wrong with a single condition variable.
- Explain release/acquire with a message-passing example and state what relaxed ordering would break.
- Explain how a futex-based mutex handles the uncontended and contended cases.
- Name two situations where broadcast is required instead of signal.
Finish the lesson
Unlocks next: Races, deadlocks, starvation and cancellation, Profiling, bottlenecks and benchmarking pitfalls


