skip to content

A second worker runs the same tick loop over one mutable entity grid; why is the result wrong even without a crash?

level: seniorimportance: must knowfreq 66%

answer

  1. a read and a write, not one step
  2. the gap between them
  3. interleavings lose an update
  4. half-updated record becomes observable
  5. legal writes, broken relationship

basics

~20 s

An imperative step is a read, a computation and a write, and its correctness assumes nothing changes the state in between. Two workers interleave those steps, so updates are lost and half-updated records become observable — while every individual write stays legal.

solid answer

~40 s

The unit an imperative loop reasons in is a **read-modify-write**, but the paradigm gives no guarantee that the three parts happen as one. With a second worker over the same grid, every interleaving is permitted, and some of them are wrong: both workers read the same energy total, both compute from it, and the second write overwrites the first, losing one update. A multi-field update is worse — a worker can read an entity after its position was written and before its velocity was, observing a combination no completed tick ever produced. Nothing crashes, because each write stores a well-formed value into a legal location. What is broken is the *relationship* between reads and writes, and the sequential loop supplied that relationship for free simply by being the only writer.

code

pseudocode · 8 lines
pseudocode
// each worker runs this over its own slice of the grid
for each entity in slice
    total = grid.energy            // read
    grid.energy = total - entity.cost   // write back what was computed

// grid.energy = 100, costs 10 and 15
// A reads 100 | B reads 100 | A writes 90 | B writes 85
// result 85 (or 90 if the writes swap) - never the sequential 75

go deeper

for a junior

Recall that updating a shared value is really a read and then a write, and that another worker can change the value in between. Be able to say that the result can silently lose one of two updates.

for a middle

Explain the interleaving with numbers, and distinguish a lost update from a torn one: a lost update leaves a plausible value, while a torn multi-field update exposes a combination no completed tick produced.

for a senior

Diagnose why such a defect reaches production — silent, delayed and interleaving-dependent — and reason about the fix by property: partition ownership, remove mutability, or carry versions, rather than reaching for a lock by reflex.

for a principal

Own where in the system shared mutable state is permitted at all. Weigh the throughput a shared grid buys against the class of defect it admits, and set the boundary so most code cannot write state another unit reads.

## The step the loop thinks is one step Almost every line in an imperative tick loop is really three operations: **read** a location, **compute** from what was read, **write** the result back. `grid.energy = grid.energy - cost` is a read, a subtraction and a write. Written as one line, it reads as one act, and the whole correctness of the loop rests on that reading: the value I computed from is still the value in the cell when I store my answer. A single-threaded loop makes that true by construction — nobody else writes. It is a guarantee nothing in the code states, nobody paid for, and everybody depends on. Adding a second worker removes it without changing a line. ## Lost updates, traced Take a grid energy total of **100**, and two workers whose slices cost **10** and **15**. The sequential answer is **75**. Now interleave: 1. Worker A reads 100. 2. Worker B reads 100. 3. Worker A computes 100 − 10 = 90 and writes 90. 4. Worker B computes 100 − 15 = 85 and writes 85. The grid holds **85**. Worker A's subtraction is gone — not corrupted, not partially applied, simply overwritten by a value computed before it existed. Swap steps 3 and 4 and the grid holds **90** instead. Both outcomes are wrong in the same way and neither is reliably reproducible, because which one you get depends on scheduling you do not control. Notice the shape of the defect: **one update out of two disappears, and the total that survives is entirely plausible.** Nothing about 85 looks like corruption. ## Torn invariants: a state no tick produced Lost updates are the easy half. The harder half is that a multi-field update has a **middle**. A tick that writes an entity's position and then its velocity passes through a moment where the position is new and the velocity is old. In a sequential loop nobody can observe that moment. With a concurrent reader, it is observable, and what the reader sees is an entity that never existed: - position and velocity disagreeing, so a derived prediction is nonsense; - an entity present in the spatial index but already removed from the grid; - a count field updated while the collection it counts is mid-edit; - two entities each holding a stale view of the other's location, so a collision is detected twice or not at all. Each of these is a broken **invariant** — a relationship between fields that every completed tick maintains and no partial tick does. Invariants are the part of the program's meaning that lives between the locations rather than in them, which is exactly the part a store made of independent cells does not protect. ## Why nothing crashes | The sequential loop gave you free | What survives a second worker | |---|---| | one writer per location | nothing: any worker may write any cell | | read-modify-write with no interruption | a read and a write with an open gap between them | | no observer of partial updates | any worker may read mid-update | | a defined order for the whole tick | an order per worker, unrelated between them | | a reproducible run | an outcome that depends on scheduling | Every write in the failing run is a well-typed value stored into a location that exists. There is no illegal operation to trap, so there is nothing to throw and nothing to log. The damage is **silent**, it is **delayed** — the wrong total is carried into the next tick as if it were right — and it is **rare**, since it needs a particular interleaving. Those three properties together are why this class of defect reaches production: it passes tests, survives review, and reproduces on a customer's machine and not on yours. ## What the property actually is The failure needs three things at once: state that is **shared** between workers, state that is **mutable**, and state identified by **location** rather than by value, so that a name carries no version and "the value I read" is not something the model can express. Take any one away and this particular read-modify-write race has nowhere to live — partition the grid so each worker owns its slice; produce new values instead of overwriting; or carry versions so a stale write can be detected. Other concurrency hazards remain in each case; this one does not. That framing is the point of the question, and it is what separates a good answer from a reflex. The reflex is "put a lock around it". A lock is a fine repair, but it works by *restoring the property the sequential loop had for free* — one writer at a time, no observer of the middle. The paradigm's own unit of meaning, a sequence of state changes applied to a shared store, simply does not compose when two sequences run at once.

  • Why is a torn multi-field update worse than a lost update?
    A lost update leaves a value that some legitimate run could have produced, so the state remains internally consistent. A torn update produces a combination no completed tick ever makes — a new position with an old velocity — so code downstream reasons from a world that never existed, and the damage spreads rather than staying in one field.
  • The failing run passes every test. What about the defect makes that unsurprising?
    Every write stores a well-formed value into a valid location, so nothing is detectably illegal. The wrong result needs a particular interleaving that a short, lightly loaded test run rarely hits, and when it does hit the output is plausible rather than obviously broken.
  • If the grid were partitioned so each worker owns its slice, would the problem go away?
    This one would: with no location written by two workers there is no interleaved read-modify-write to lose. The difficulty moves to the boundaries — entities crossing between slices, and any aggregate both workers update — and those boundaries then need the same analysis the whole grid used to.
  • Why is 'add a lock' an answer about the symptom rather than the property?
    Because a lock re-imposes what the sequential loop already had: a single writer at a time and no observer of a partial update. It repairs the run without changing the reason it broke, which is that a sequence of state changes over a shared store is not composable with another such sequence.

saying these in an interview costs you the question

  • Says the risk is only crashes, so no crash means no corruption
  • Claims a single-line increment is inherently indivisible
  • Believes the damage is always confined to one wrong field
  • Assumes the sequential loop's ordering still holds across workers
  • Thinks a rare interleaving will reproduce reliably under a debugger