ADVANCED OPERATING SYSTEMS · LECTURE 6 RECAP · 10 SEPTEMBER 2026 · READ WHILE THE LECTURE IS STILL WARM

From one bit
to a queue

A spinlock starts as one bit and one atomic instruction. The lecture is about discovering, one failure mode at a time, that the real problem is not mutual exclusion. It is what dozens of waiting processors force the memory system to do.

The path: define what “good” means → understand the atomic primitives → TAS → cache-local polling → backoff → ticket locks → Anderson/Graunke–Thakkar → locality and NUMA → MCS → read the performance graph correctly.

TASone hot word
TTASread locally
Backofftry less often
Ticketknow your turn
Andersonseparate wait spots
MCSdirected handoff

1. First decide what a good lock is

It is easy to reduce this topic to “which lock is fastest?” That loses the point. The lecture gives you several axes because improving one can damage another.

MetricMeaningThe question to ask
LatencyCost of acquiring and releasing an uncontended lock.If nobody else wants it, how much machinery did I pay for?
Handoff delayTime from one owner releasing to a waiting processor acquiring.When the lock becomes free, how quickly does useful work resume?
Contention / network loadCompetition and traffic induced in caches, buses, memory modules, and the interconnect.Does waiting itself slow the machine — including the current lock holder?
SpaceState required by the lock and its waiters.Is this one word, an array sized by processors, or per-waiter state?
FairnessWhether arrivals obtain service in a predictable order.Can an old waiter starve while newcomers repeatedly win?
LocalityWhere the locations being polled physically live.Am I spinning on a cache-local / node-local location or hammering remote memory?
Preemption sensitivityWhat happens if a queued processor stops running.Does everybody behind a descheduled waiter get stuck?

The lecture explicitly starts with latency, handoff delay, contention/bandwidth, and space; fairness, locality, preemption, and available atomic primitives become unavoidable as the algorithms evolve.

Contention is not the same thing as bandwidth

Bandwidth consumption is how much traffic you generate. Contention is what happens when requests compete for the same finite resource and delay one another. They often rise together, but they are not identical.

A lock can generate only a few bytes per operation and still be pathological if every processor targets the same serialized cache line. Conversely, a machine can move a lot of data across independent links without severe contention. TAS gives you the bad combination: lots of repeated traffic, all focused on the same object.

2. The atomic instructions are different tools

The names are close enough to blur together in a lecture. Do not blur them. The algorithms later depend on the distinctions.

PrimitiveConceptual operationWhy it matters here
test_and_set(x)Atomically set x = 1, return the old value.The simplest spinlock: whoever observes old 0 wins.
fetch_and_store(x,v) / exchangeAtomically write v, return old x.MCS uses this to replace the queue tail and learn its predecessor.
fetch_and_add(x,1)Atomically increment, return the previous value.Perfect ticket dispenser: unique monotonically increasing positions.
compare_and_swap(x,e,n)Only if x == e, replace it with n; report success / old value.Conditional update. MCS uses it on release to prove “I really am still the tail” before clearing an apparently empty queue.
TAS vs CAS in one sentence. TAS writes unconditionally and tells you what used to be there. CAS writes only if the state is still exactly what you expected. That “only if nobody changed this behind my back” is why CAS solves races that TAS cannot express cleanly.

3. Start stupid: spin on test-and-set

acquire:
    while test_and_set(lock) == BUSY:
        pass

release:
    lock = FREE

This is beautiful under no contention. One atomic operation, tiny state, very low latency. That is why simple locks never disappear from the design space.

Now add many waiters. Every failed test_and_set is a read-modify-write attempt on the same location. On a coherent cache machine that means repeatedly trying to obtain exclusive ownership of the lock line. On a bus or interconnection network it means a stream of transactions aimed at one hot address.

P0 owns lock P1 ─atomic─┐ P2 ─atomic─┼──▶ [ LOCK CACHE LINE ] ◀── P0 eventually needs it to unlock P3 ─atomic─┼ P4 ─atomic─┘ The waiters are not passive. They can delay the owner.

This is the first systems lesson: waiting has a topology. A logically idle processor can be very physically busy.

4. Test-test-and-set: exploit the cache

The obvious waste is performing an atomic write attempt when we already know the lock is busy. So first poll it with ordinary reads, and only perform TAS after observing it free:

for (;;) {
    while (load(lock) == BUSY)
        pause();              // ordinary reads

    if (test_and_set(lock) == FREE)
        break;
}

On a cache-coherent machine, once a waiter has a shared copy of the lock line, repeated reads can hit in its local cache. That is what “spin on cache” means. It is not magic private state: it is a cached copy whose freshness is maintained by coherence.

Write-invalidate vs write-update

Suppose ten processors cache lock = BUSY. The owner releases.

With write-invalidate, the owner's write invalidates the other cached copies. On their next load, waiters miss and fetch the new value. With write-update, the new value is broadcast into their copies. Either way, the coherence mechanism is doing work to make the release visible. Invalidate tends to defer traffic until waiters next read; update pushes the new value immediately.

TTAS eliminates the continuous RMW storm while the lock remains busy, but release still creates a herd: everybody can discover FREE at roughly the same time and race into TAS. One wins; the others lose and go back to reading.

What if the architecture is not cache coherent? Then “keep rereading my cached copy until somebody changes it” is not a valid synchronization mechanism: your copy may simply stay stale. Either your polls must reach a shared location, software must explicitly maintain coherence, or — better — the algorithm can arrange for each waiter to spin on a location that is physically local but still writable by its predecessor. MCS is built to make that last pattern work.

5. Backoff: do not solve congestion by generating more congestion

Another repair does not change the lock representation at all. It changes when losers retry.

delay = 1
while test_and_set(lock) == BUSY:
    pause(randomized(delay))
    delay = min(2 * delay, MAX_DELAY)

The networking analogy is exact enough to be useful: many agents collide on one shared resource; immediate synchronized retry recreates the collision; randomized exponential backoff spreads attempts out.

This reduces interconnect pressure, but now your delay policy directly affects handoff latency. If everyone happens to be sleeping when the lock becomes free, the resource sits idle. Backoff is therefore not a free improvement. It trades traffic against responsiveness.

Why fairness can get ugly

Suppose an old waiter has failed repeatedly and backed off to a long delay. A newcomer arrives with a tiny initial delay just after the lock becomes free. The newcomer can win before the old waiter even checks again. More failures can literally mean “I look less often,” creating newcomer bias and no FIFO guarantee.

Dynamic backoff is a prediction problem. You are trying to estimate when another attempt is worth paying for. Too short: traffic storm. Too long: idle lock and unfairness. This sets up the next move: if we knew our position, we could make a much better prediction.

6. Ticket locks: turn a race into a line

Use two monotonically increasing counters:

my = fetch_and_add(next_ticket, 1)
while now_serving != my:
    wait

// critical section
now_serving++
next_ticket hands out: 41 42 43 44 P1 P2 P3 P4 now_serving = 41 P1 enters. P2 knows it is exactly 1 handoff away. P4 knows it is exactly 3 handoffs away.

This gives FIFO ordering and constant-size lock state. More importantly, position gives information. If you are ticket 44 and now_serving is 41, checking on every cycle is pointless. Proportional backoff can make the delay roughly proportional to your distance from the head.

Why not just use timestamps?

A timestamp sounds like a ticket because it gives an order. But the lock needs more than “time usually increases.” It needs a unique, globally agreed ordering operation under simultaneous arrivals. Two cores can read the same timestamp; clocks can have architecture-specific synchronization semantics; and you still need shared state that says which timestamp is currently entitled to enter. fetch_and_add directly performs the thing we actually need: atomically assign one unique position in a total order.

Is the ticket board still a hot spot?

Yes. This is the important limitation. Everybody ultimately watches now_serving. With coherent caches they may poll it locally while it is unchanged, but every increment must become visible to all those cached copies. Without coherent caching, repeated polling may be remote traffic.

So ticket locks solve fairness and make backoff intelligent, but they have not yet solved the structural problem that all waiters care about one shared word.

7. Central coordination is not itself the enemy

This is a subtle point worth keeping. Anderson and MCS still use a central atomic operation when a processor joins the queue. MCS atomically swaps the tail. Anderson atomically allocates a slot.

That is okay because the central operation happens a bounded number of times per acquisition. The pathological thing was unbounded repeated central traffic while waiting.

Scalable pattern:
coordinate centrally once → learn who/where to wait for → spin locally → directed handoff

This distinction is more reusable than any individual lock algorithm.

8. Anderson: give every waiter its own place to spin

Anderson's queue lock uses an array of flags. A fetch-and-increment assigns each arriving processor a slot. Exactly one slot says “has lock”; the others say “must wait.” Each processor spins on its assigned slot, and release marks the next slot runnable.

slots: [ wait ][ wait ][ GO ][ wait ][ wait ][ wait ] ▲ ▲ owner successor Each waiter polls a different slot. Release touches only the next slot.

On a coherent-cache machine, put different slots on different cache lines so one handoff does not invalidate unrelated waiters. On physically distributed shared memory, place slots in different / preferably local memory modules.

Why “different cache lines” matters

If eight logical flags share one 64-byte cache line, a write to P3's flag invalidates the line containing P4, P5, and P6's flags too. That is false sharing: logically independent state becomes physically coupled by the coherence granularity. Your elegant local-spinning algorithm collapses back toward shared traffic.

“How many cache lines do we even have?”

You are not likely to exhaust the cache-line namespace. The real issue is footprint. Padding one byte of useful flag to an entire 64-byte line means 64 waiters consume 4 KiB per lock. Multiply by thousands of locks and the representation becomes absurd. That is why space becomes a first-class metric.

Anderson's lock buys clean local spinning with an array sized to the maximum number of processors/waiters for each lock. The Graunke–Thakkar lock is another array-based point in this design space; among other differences it uses fetch-and-store rather than requiring fetch-and-increment. For this lecture's main arc, the important fact is simply that both distribute the spin locations, but pay substantial per-lock space.

9. NUMA makes “where?” impossible to ignore

So far “memory” sounded like one place. On NUMA systems it is not. A core reaches some memory through a nearby controller and other memory across sockets / interconnect links. Two loads with the same instruction count can have very different cost.

Now every lock algorithm gets another question:

Where does the waiter spin, physically?

A global ticket word can sit near one socket and far from another. An Anderson slot can be placed near its waiter. A per-waiter node can live in memory local to its processor and still be shared so that another processor can perform the one write that wakes it.

This is the bridge into MCS. We want FIFO order and local spinning without allocating a giant array inside every lock.

10. MCS: make the queue itself distributed

An MCS lock is almost comically small:

lock = pointer to tail node (or NIL)

qnode:
    next
    locked

The lock object does not contain all waiter state. Each contender brings a node. Those nodes form a linked list:

lock.tail ───────────────────────────────┐ ▼ P0 node ──next──▶ P1 node ──next──▶ P2 node (owner) waits waits on P1.locked on P2.locked handoff: P0 writes P1.locked = false P2 learns nothing yet — and does not need to.

That last sentence is the point. With 64 waiters, a release does not need to become an event observed by 64 processors. It needs to become an event observed by one successor.

Acquisition, line by line

acquire(L, me):
    me.next = NIL
    pred = atomic_exchange(L.tail, me)

    if pred != NIL:
        me.locked = true
        pred.next = me
        while me.locked:
            spin locally

    // if pred == NIL, the queue was empty: I own the lock

1. me.next = NIL. Initialize your queue node.

2. pred = exchange(tail, me). This is the clever atomic step. In one indivisible operation, you make yourself the new tail and receive the identity of the old tail. That old tail is your predecessor.

3. If there was no predecessor, you acquired immediately. No queue existed.

4. Otherwise, link yourself behind the predecessor. Write pred.next = me.

5. Spin on your own flag. The predecessor will eventually clear exactly that flag. On a cache-coherent machine the repeated reads can be cache-local. On distributed shared memory, your node can be allocated in your local memory module.

The MCS scaling claim. Queue insertion uses one central atomic exchange, but ordinary waiting does not keep touching the tail. Handoff is predecessor → successor. The original paper describes this as a constant number of remote references per acquisition, independent of the number of contenders.

Release looks weird because of one race

release(L, me):
    if me.next == NIL:
        if compare_and_swap(L.tail, me, NIL):
            return

        while me.next == NIL:
            spin

    me.next.locked = false

If me.next != NIL, life is easy: wake that successor.

The interesting case is me.next == NIL. You might conclude “nobody is behind me; clear the tail.” But there is a tiny window where a successor has already atomically swapped itself into L.tail and has not yet executed me.next = successor.

P0 (releasing) P1 (acquiring) reads P0.next == NIL exchange(tail, P1) // P1 is now tail // ...has not linked yet... CAS(tail, P0, NIL) ── fails P0 now KNOWS someone arrived. It waits until P1 publishes: P0.next = P1 then writes P1.locked = false.

This is precisely why CAS is the right primitive. P0 is allowed to clear tail only if the tail is still P0. The CAS packages “verify nobody changed it” and “clear it” into one atomic act.

Why not plain test-and-set here?

Because the release race is not “who can set a bit first?” It is “clear this pointer only if it still has the exact value I previously observed.” That is conditional state transition territory. CAS expresses the invariant directly.

11. The ZooKeeper analogy was good

A common ZooKeeper lock recipe creates sequential nodes and watches only the immediately preceding node instead of having every waiter watch one global lock node. The substrate and failure model are completely different, but the structural move is the same:

establish a total order → each waiter depends on one predecessor → predecessor departure triggers the next

That analogy is useful because it reveals that MCS is not primarily “a linked-list trick.” It is a way of turning broadcast competition into directed handoff.

12. Read the Butterfly graph as an experiment, not decoration

The BBN Butterfly used in the paper was a shared-memory multiprocessor with a multistage interconnection network, physically distributed memory, no cache coherence, and comparatively expensive atomic operations. That makes it a harsh test for algorithms that generate remote traffic.

The graph from lecture uses an empty critical section. That choice is deliberate. Useful work inside the lock is essentially removed, so the measured time exposes the synchronization machinery itself under contention.

The qualitative result to keep:

AlgorithmWhat happens as contenders grow on Butterfly?Why?
TAS + exponential backoffFar better than raw TAS, but still induces appreciable network load.Retries are rarer, not structurally localized.
Ticket + proportional backoffScales surprisingly well.Queue distance suppresses unnecessary polls, though state remains central.
AndersonDistributed spinning helps, but the evaluated implementation rises noticeably.Its required atomic / implementation costs on this machine matter; the paper emphasizes that hardware primitives change the ranking.
MCSNear-flat under increasing contention and very low induced network latency.Distributed state plus local spinning plus one directed handoff.

The paper's network-latency experiment is even more revealing than the lock timing graph: central TAS/ticket designs can increase latency for unrelated traffic because they create a hot network destination. MCS keeps both the hot-spot effect and unrelated-network impact tiny because waiters do not repeatedly access remote queue state.

So what is a “Butterfly”? And what is a “dance hall”?

Butterfly here is the BBN Butterfly multiprocessor, named for its interconnection network topology. Processors and distributed memory communicate through a multistage network; a remote memory operation traverses that network.

Dance hall is an architectural metaphor from this literature: processors are on one side, shared memory on the other, and shared locations are roughly equally remote rather than having useful processor-local placement. The MCS paper argues that locality is valuable: if wait flags can live near the processors that poll them, software can avoid synchronization-induced network contention.

13. Why didn't everybody just use MCS?

Because “scales beautifully” is not the same as “dominates every operating point.”

MCS has more machinery than a one-word TAS lock on the uncontended path. The caller needs a queue node that must survive from acquisition through release. Strict queueing also means preemption can hurt: if the next waiter in FIFO order is descheduled, processors behind it cannot simply barge past. And systems APIs may have been designed around a tiny opaque lock word rather than “lock plus caller-provided node.”

This is not merely historical. Modern Linux's queued spinlock is explicitly MCS-inspired, but its implementation comments explain that it modifies the classic design to preserve a 4-byte spinlock_t and the existing lock API. That is a nice real-world answer to the lecture question: good algorithms collide with ABI, memory footprint, fast paths, virtualization, preemption, and decades of surrounding code.

14. The whole lecture in one compression pass

LockMain ideaWhat it fixesWhat remains
TASRace atomically on one bit.Mutual exclusion with minimal uncontended latency.Every waiter pounds one location.
TTASPoll with reads; use atomic only when free.Removes continuous write-intent storm while held.Release herd; depends on coherence for cheap cached polling.
BackoffRetry less often and desynchronize retries.Reduces traffic.Delay tuning, idle gaps, weak fairness.
TicketGive every arrival an ordered number.FIFO fairness; enables proportional waiting.Everybody still cares about now_serving.
AndersonGive every queue position a separate flag.Local spinning and directed wakeup.O(P) padded state per lock; primitive/topology dependence.
MCSLinked queue of waiter-owned nodes.FIFO + local spinning + O(1) lock object + directed handoff.Per-waiter node/API complexity; preemption and fast-path tradeoffs.
The sentence worth retaining a month from now: scalable synchronization is mostly the art of making the number of other waiters irrelevant to what one waiting processor repeatedly does.

15. Five checks before you call this learned

1. Why is TTAS better than TAS even though they use the same atomic operation to actually acquire?

Because they differ in the losing path. TAS losers repeatedly issue atomic RMWs; TTAS losers mostly issue ordinary reads that can remain cache-local while the lock is held. The acquisition primitive is the same; the waiting traffic is not.

2. Why can exponential backoff reduce contention and still make handoff delay worse?

Because reducing probe frequency means there may be no waiter looking precisely when the lock becomes free. Backoff trades less traffic for less immediate responsiveness.

3. What does a ticket lock know that an exponential-backoff TAS lock does not?

Queue position. Ticket distance estimates how many critical-section handoffs must occur before you can win, which makes proportional backoff possible and gives FIFO fairness.

4. MCS still atomically modifies one shared tail. Why is that not the same hotspot problem as TAS?

Because each contender touches the central tail a bounded number of times while joining the queue, then stops touching it while waiting. TAS repeatedly attacks the central word for the entire wait.

5. In MCS release, why can me.next == NIL coexist with another waiter already being in the queue?

Because enqueue has two distinct steps: the new waiter first atomically swaps itself into the global tail, then writes itself into its predecessor's next. The releaser can observe the system in between those steps. CAS on the tail detects that intermediate state safely.

Sources and scope

This recap follows the lecture's progression through MCS and fills only gaps needed to make that progression cohere. It deliberately stops before the barrier algorithms.

Mellor-Crummey & Scott, “Algorithms for Scalable Synchronization on Shared-Memory Multiprocessors” (1991)
Linux qspinlock.c — modern MCS-inspired queued spinlock
Earlier narrow explainer: “Why can a correct spinlock scale badly?”