skip to content

Explain the copy-on-write technique for sharing a data structure between many concurrent readers and an occasional writer: what invariant lets readers run without locking, and what is its cost model?

level: middleimportance: should knowfreq 40%

answer

  1. published version never mutated
  2. reader: one atomic reference read, then lock-free
  3. writer: copy, mutate copy, atomically swap
  4. writers still need a lock or CAS retry
  5. O(n) per write, 2x memory, stale reads OK?

basics

~20 s

Readers read a snapshot that is never modified in place. A writer copies the structure, changes the copy, and atomically swaps the shared reference. Readers need no lock and see a consistent, possibly slightly stale, version. Cost: a full copy per write, so only for read-mostly data.

solid answer

~60 s

The invariant is: **a published version is never mutated**. A single shared reference points at the current immutable version. Readers take that reference once and then read freely — no lock, no retry — because their version cannot change under them. A writer copies the current version, applies its change to the private copy, and atomically publishes the new reference. Consequences: - Readers never block and never block writers. Iteration is safe and cannot fail mid-traversal. - Readers may see a stale version — the one current when they took the reference. If that is unacceptable you need a different structure. - **Writers still need mutual exclusion among themselves**, or a compare-and-swap retry loop. Naive read-copy-swap by two writers loses one update. - Cost is O(n) time and a transient doubling of memory per write, so it is only sound when reads massively outnumber writes: routing tables, config, listener lists, feature flags. Persistent data structures reduce the copy to O(log n) by sharing unchanged subtrees, which is safe precisely because published versions are immutable. Reclaiming old versions needs garbage collection or a grace-period scheme.

code

text · 10 lines
text
W1: old = load(current)      # {a}
W2: old = load(current)      # {a}
W1: new = copy(old)+b -> store(current, {a,b})
W2: new = copy(old)+c -> store(current, {a,c})    # b is gone

# fix: compare-and-swap and retry
loop:
  old = load(current)
  new = copy(old); mutate(new)
  if compare_and_swap(current, old, new): break

go deeper

for a junior

State the mechanics: readers use a snapshot, the writer copies, changes the copy, and swaps the pointer, so readers never need a lock.

for a middle

Add the invariant (published versions are never mutated), the staleness consequence, and the O(n)-per-write cost.

for a senior

Emphasize that writers still need mutual exclusion or compare-and-swap retry, the memory spike and reclamation of old versions, and the read/write ratio that justifies the choice.

for a principal

Compare against fine-grained locking and partitioned ownership on write throughput, bring in persistent structures for O(log n) updates, and be explicit that the consistency model offered is snapshot, not linearizable.

## The construction Copy-on-write splits a mutable shared structure into two things: an immutable snapshot, and one mutable reference that says which snapshot is current. reader: s = load(current) # one atomic read use(s) ... # s never changes; no lock writer: under writer_lock: old = load(current) new = copy(old); mutate(new) store(current, new) # atomic publish Everything follows from the invariant that whatever current has ever pointed at is never modified again. ## Why readers need nothing A reader performs exactly one synchronizing operation: reading the reference. After that it holds a private handle to a structure that no one will touch, so any number of reads across any interleaving return mutually consistent data. This is why copy-on-write collections are the standard answer for listener/observer lists: iteration cannot observe a concurrent modification, since the version being iterated is frozen. It also gives a genuinely useful property that locking does not: a reader can hold a consistent multi-field snapshot for as long as it likes without blocking writers. Under a lock, a long read blocks updates; here it merely keeps an old version alive. ## What readers give up Staleness. A reader that took the reference before a publish will not see that update, and two readers may act on different versions simultaneously. Copy-on-write therefore provides *snapshot consistency*, not linearizable freshness. It suits data where being a few milliseconds behind is fine — configuration, routing, permissions caches — and is wrong for anything where a decision must reflect the latest write, such as a balance check. A subtle version of the same trap: a read-modify-write built on top (read the snapshot, compute, publish a new one) is not atomic unless the publish is guarded, which is the writer problem below. ## The writer side is the part people forget Copy-on-write makes readers lock-free; it says nothing about writers. Two writers that each copy the same old version and then publish will produce lost updates — the second publish overwrites the first entirely, which is worse than a normal lost update because it discards a whole structure. Fix it either with a lock held for the copy-mutate-publish sequence, or with a compare-and-swap on the reference plus retry from the freshly loaded version. Write throughput is the hard limit of the technique. Each write is O(n) copying plus allocation; with a burst of writes you get quadratic total work and a memory spike, and under a compare-and-swap loop you also get retries that throw away completed copies. A structure with frequent writes needs a different design (fine-grained locks, a concurrent structure, or partitioned ownership). ## Memory and reclamation During a write, both versions exist, so peak memory is roughly double the structure. Old versions must stay alive while any reader still holds them; in a garbage-collected runtime this happens automatically — the old version becomes unreachable when the last reader drops it. Without automatic reclamation you need a grace-period scheme that defers freeing until all pre-existing readers have finished, which is the essence of read-copy-update as used in operating-system kernels. A long-running reader that keeps a snapshot alive pins that memory, which is a real leak vector when snapshots are large and readers are long-lived. ## Reducing the copy cost Persistent (structurally shared) data structures make the copy proportional to the changed path rather than the whole structure — for a tree, O(log n) new nodes with the rest shared with the previous version. Sharing is only safe because old versions are immutable, so persistence and copy-on-write are natural partners. Batching helps too: apply many pending changes to one copy rather than copying per change. ## When to choose it Use it when reads vastly outnumber writes, the structure is small to moderate, and stale-by-a-moment is acceptable. Avoid it for write-heavy workloads, very large structures, or anywhere a reader must observe the newest value. State the read/write ratio when you propose it — that ratio is the whole argument.

  • Readers are lock-free under copy-on-write. Are writers?
    No. Writers must be serialized among themselves, either by a lock held across copy-mutate-publish or by a compare-and-swap on the shared reference with retry from the reloaded version. Without that, two writers that copied the same base version each publish a structure missing the other's change, losing an update wholesale.
  • How do persistent data structures change the cost model?
    They replace the whole-structure copy with copying only the path affected by the change and sharing every untouched subtree with the previous version, so a tree update costs about O(log n) new nodes instead of O(n). Sharing is safe exactly because published versions are immutable. Write cost drops enough that copy-on-write becomes viable at moderate write rates and larger sizes.

Publishing a new edition of a printed directory. Everyone holding last month's copy keeps reading it happily and consistently; the publisher prints a whole new edition rather than sending people into offices to correct their copies.

saying these in an interview costs you the question

  • Thinking copy-on-write makes writes cheap or lock-free as well as reads.
  • Ignoring that readers can act on a stale snapshot and using it where freshness is required.
  • Doing read-copy-publish from multiple writers with no lock or compare-and-swap, and losing whole updates.
  • Using it for a write-heavy or very large structure, where copying dominates.
  • Assuming old versions are freed immediately, when a long-lived reader can pin them.

context