9.3Maintaining Liveness: Deadlock, Livelock, Starvation

book pp. 200–206 · ~5 min read

  • safety vs liveness
  • deadlock
  • virtual networks vs channels
  • livelock
  • Peekaboo problem
  • starvation

Chapter 2’s invariants were all safety : nothing bad ever happens. Facetiously, safety is easy — an unplugged computer never does anything incorrect. The other half of correctness is liveness : something good eventually happens, which means preventing three distinct failures.

9.3.1 Deadlock

Deadlock : actors waiting on each other in a cycle, forever. The canonical shape:

AXYBA requests XB holds XB requests YA holds Y
Figure 9.3 (recreated): a cyclic resource-dependence graph. Partial deadlocks metastasize — any node that waits on A or B joins the standstill.

Coherence implementations can deadlock three ways:

Protocol deadlock

A controller waits for a message that will never be sent — a design bug. Example: skip the Put-Ack wait after a PutS and go straight to I; if the directory’s Inv crosses that PutS in flight, the requesting core waits for an Inv-Ack that C0 (already in I) will never send. Usually an untested race.

Cache resource deadlock

Allocation before action: if the shared TBEs serve both a core’s own requests and other cores’ requests, a core that fills them with its own can’t service anyone. All cores in that state ⇒ system deadlock.

Network deadlock (protocol-dependent)

Message classes blocking each other in shared FIFOs — prevented structurally by the three invariants below. (Buggy-routing deadlocks are the protocol-independent kind — virtual channels ’ territory.)

The three no-deadlock invariants

For the directory chain request → forwarded request → response:

  1. One network per message class — physical or virtual; a message of one class must never sit stuck behind another class in a FIFO.
  2. Respect the dependence order — if class A can cause class B, a controller may not stall B’s processing while waiting for an A. (No stalling a forwarded request to wait for a request; no stalling a response to wait for a forwarded request.)
  3. The last class always sinks — a response must be removable from its queue unconditionally, blocked by nothing.

Requests may stall waiting on the other classes — harmlessly, because the number of forwards and responses is bounded by outstanding transactions. Cycles are what these rules make impossible.

9.3.2 Livelock

Livelock : everyone keeps acting, nobody progresses — starvation’s total form. NACK-retry storms are the classic cause (this primer’s protocols have no NACKs), so the star exhibit is the window of vulnerability . Table 9.1’s trace, in a snooping protocol:

CycleEvent (all for block B)C1’s state for B
0Initial stateI
1Load request; issue GetS to busISAD
2Observe Own-GetS on busISD
3Observe Other-GetM on busISDI
4Receive data for Own-GetSI
5Re-issue GetS to busISAD
6Observe Own-GetS on busISD
7Observe Other-GetM on busISDI
8Receive data for Own-GetSI
9Etc. — the load never completes

Table 9.1 (recreated): the window-of-vulnerability loop. Perversely, it bites hardest on contended blocks — where every core loops at once: system livelock.

The fix is almost free: perform at least one load when the data arrives — logically the load occurs when the GetS was ordered, so coherence is safe. But consistency is not automatically safe. Step through why:

C0: block AM[0]
C0: block BM[0]
C1: block AI
C1: block BI
C1 registersr1=?, r2=?

Non-stalling §8.7.2 protocol; directory actions elided.

1 / 8The program (Table 9.2)

A = B = 0. C0: Store A=1, then Store B=1. C1: Prefetch A for read, then Load r1=B, then Load r2=A. Under SC and TSO, r1=1 ∧ r2=0 is the one illegal outcome — if C1 sees the second store (B=1), it must see the first (A=1).

The Peekaboo rule

Perform the window-of-vulnerability access iff it was the oldest unperformed load/store in program order when its coherence request was issued. Intuition: no problem can arise if a core issues coherence requests one at a time, in demand-miss order. The same discipline covers stores caught in IMDS / IMDSI / IMDI — a GetM issuer must perform at least one store when data arrives and forward the newly written data to the cores that requested the block in between. The rule comes from Manerkar et al.’s CCICheck, a tool for formally verifying the coherence–consistency interface — §11.3 shows the machinery behind it.

9.3.3 Starvation

Starvation : some cores progress, others never do. Two root causes:

  • Unfair arbitration. A fixed-priority bus (C2 yields to C1, C3 to both, …) can lock a low-priority core out indefinitely. Known problem, known cure: fair arbitration .
  • Misused NACKs . NACK-and-retry looks like a seductively simple answer to in-progress-transaction races (the SGI Origin uses one for its Upgrade race) — but guaranteeing a NACKed request eventually succeeds, under arbitrary contention, is genuinely hard. One of the primer’s authors confesses to having designed a NACK protocol that starved.

Check yourself

1.Why does the book call safety alone 'facetiously easy'?

2.Name the three flavors of deadlock in coherence implementations.

3.What three invariants eliminate protocol-dependent network deadlock (for request → forwarded request → response)?

4.Virtual networks vs. virtual channels — what's the difference?

5.In the window-of-vulnerability livelock (Table 9.1), C1 loops IS^AD → IS^D → IS^D_I → I forever. Why is the simple fix ('perform one load when data arrives') both correct for coherence AND dangerous for consistency?

6.What are the two root causes of starvation, and their remedies?

6 questions