5.4Sequential Consistency for Data-Race-Free Programs

book pp. 68–72 · ~5 min read

  • data race
  • DRF
  • SC for DRF
  • undecidable FENCE placement

Having the cake and eating it

Children and computer architects would like to have their cake and eat it too: reason with the (relatively) intuitive model of SC while getting the performance of a relaxed model like XC. For the important class of data-race-free (DRF) programs, both are simultaneously possible.

Informally, a data race occurs when two threads access the same location, at least one access is a write, and no synchronization intervenes. Races are often (not always) programming errors, and many programmers aim to write DRF programs anyway. SC for DRF is a contract with two sides:

  • Programmers ensure their programs are DRF under SC — correctly synchronized, with every synchronization operation labeled as such;
  • Implementors map the labeled synchronization to the FENCEs and RMWs the relaxed model supplies, guaranteeing that all executions of DRF programs are SC executions.

XC and most commercial relaxed models have the FENCEs and RMWs needed to “recover SC” this way — and the approach is the foundation of the Java and C++ memory models (§5.9).

Two examples

In both tables below, C1 stores data1 and data2 inside a FENCEd, locked critical section, while C2 loads the two locations in the opposite order. The difference: whether C2 synchronizes.

Table 5.7 — C2 does not synchronize. Its loads run concurrently with C1’s stores. XC may reorder S1/S2 (no FENCE between them) and L1/L2, so four outcomes are possible — including one no SC execution allows:

Table 5.7 (core): C2 reads with no synchronization — a data race

Core C1 (critical section)Core C2 (no synchronization)
F12: FENCEL1: r2 = data2;
S1: data1 = NEW;L2: r1 = data1;
S2: data2 = NEW;
F13: FENCE

Initially data1 = 0, data2 = 0. Table 5.7 (core of): C1 stores inside its FENCEd critical section; C2 reads with no synchronization at all.

Model:

Possible outcomes (r1, r2) under XC (FENCEs only):

(NEW, NEW)SC
16 executions — show oneF12 → S1 → S2 → F13 → L1 → L2
(NEW, 0)SC
14 executions — show oneF12 → S1 → L1 → S2 → F13 → L2
(0, 0)SC
16 executions — show oneF12 → L1 → L2 → S1 → S2 → F13
(0, NEW)not SC
14 executions — show oneF12 → L2 → S1 → S2 → F13 → L1

XC enumeration reorders each thread freely EXCEPT across a FENCE and for same-address Load→Load / Load→Store / Store→Store; (bypass) = the load read its own thread's program-order-earlier store even though that store enters memory order later (the extended value rule).

The (r1, r2) = (0, NEW) outcome — see it via the witness, e.g. S2, L1, L2, S1 — is XC’s reordering exposed by the race (S1 races with L2, S2 with L1).

Table 5.8 — C2 acquires the same lock. Now C1’s critical section runs entirely before C2’s, or vice versa:

Core C1Core C2Comments
F11: FENCE
A11: acquire(lock)
F12: FENCE
/* Initially, data1 & data2 = 0 */
S1: data1 = NEW;
S2: data2 = NEW;
F13: FENCE
R11: release(lock)
F14: FENCE
F21: FENCE
A21: acquire(lock)
F22: FENCE
L1: r2 = data2;
L2: r1 = data1;
/* Two possible outcomes under XC: (r1, r2) = (0, 0) or (NEW, NEW) — same as SC */
F23: FENCE
R22: release(lock)
F24: FENCE

Only (0, 0) and (NEW, NEW) remain — the SC outcomes — and this holds whether or not C1 reorders its stores or C2 its loads:

“A tree falls in the woods (reordered stores), but no one hears it (no concurrent loads).”

Generalizing from the two examples, SC for DRF claims: either an execution has data races that expose XC’s reordering, or the execution is data-race-free and indistinguishable from an SC execution.

The definitions, precisely

For XC: require that every synchronization operation is preceded and succeeded by a FENCE, as in Table 5.8. With that, XC supports SC for DRF — the proof is beyond the book’s scope, but the intuition is exactly the two tables above.

The catches

And isn’t there always a catch? Guaranteeing DRF at high performance — without labeling too many operations as synchronization — is hard, for two reasons.

1. Optimal labeling is undecidable. Figure 5.4’s gadget makes FENCE necessity depend on the halting problem:

C1C2Does thiscode halt?F1: FENCEX = 1F2: FENCEF3: FENCE?X = 2F4: FENCE?race?C2’s FENCEs F3 and F4 are necessary only if C1 executes “X = 1” —and determining that means solving the undecidable halting problem.

Figure 5.4 (recreated): optimal placement of FENCEs is undecidable.

The practical escape is conservatism: add a FENCE whenever unsure — always correct, possibly slower. In the limit, surround every memory operation with FENCEs and any program behaves SC.

2. Bugs happen. A program with an accidental data race loses the guarantee — after a race, execution may stop obeying SC, and the programmer must reason with the relaxed model itself. The good news: executions obey SC at least until the first data race, so debugging can start with SC reasoning alone.

Two ways to reason

A programmer on a relaxed memory system can:

  1. reason directly with the model’s rules (Table 5.5 and friends), or
  2. insert enough synchronization to eliminate data races (synchronization races — competing lock acquires — are still allowed) and reason entirely in SC.

The book recommends option 2 almost always, leaving option 1 to experts writing synchronization libraries and device drivers.

Check yourself

1."SC for DRF" is a contract. Who promises what?

2.Tables 5.7 and 5.8 run the same stores and loads, yet the locked version (5.8) has only outcomes (0, 0) and (NEW, NEW). Why?

3.Precisely when are two conflicting data operations Di <m Dj NOT a data race?

4.Why is optimal FENCE placement (equivalently, minimal synchronization labeling) undecidable?

5.A program has a data race due to a bug. What does SC-for-DRF still guarantee about its executions?

5 questions