What must be true of a counted handle for its count to be updated non-atomically, and what fails if that breaks?
answer
- the instruction only prevents one thing
- not your handle - every handle
- one thread, or one lock
- lost increment versus lost decrement
- early free one way, leak the other
basics
~20 sA count may skip atomic instructions only when no two threads can update it at once: every handle must stay in one thread, or under one lock. Otherwise a lost increment frees the object early; a lost decrement leaks it.
solid answer
~50 sThe atomic instruction exists solely to stop two concurrent updates from overwriting each other, so it can be dropped exactly when concurrency is impossible. That means every owning handle to the object — not just the one in your hand — is confined to a single thread, or every update already happens inside the same lock. This is why some schemes offer two handle kinds, a cheap thread-confined one and an atomic shared one, and why the thread-confined kind is the right default for objects that never escape. The failure when confinement breaks is not a clean error. A lost increment leaves the count too low, so it reaches zero while a live handle still exists and the object is destroyed underneath its user. A lost decrement leaves the count too high and the object simply never dies. Both are timing-dependent and surface far from the escape.
go deeper
Hold on to the condition: a count can skip atomic instructions only when no two threads can touch it at once. Publishing an object to another thread removes that guarantee for good.
Be able to walk the interleaving of two plain increments and name both outcomes: a lost increment ends in an early free and a dangling handle, a lost decrement ends in an object that never dies.
Show that you police the publication point rather than the update site — the object must be the shared kind before any handle escapes — and that you expect these bugs to appear far from their cause and only under load.
Treat confinement as a design constraint with throughput value, not just a safety rule: which objects are allowed to be published, and by whom, decides how much of the fleet's fast path pays the atomic tax.
## What the atomic instruction is actually buying An increment is three operations — read the word, add one, write it back. The only thing an atomic read-modify-write adds is **indivisibility**: no other thread can observe or modify the word between the read and the write. That guarantee has exactly one purpose, to prevent a concurrent update from being overwritten. So the guarantee is worth paying for precisely when concurrent updates are possible, and worth nothing when they are not. That gives a sharp rule. A count may be updated with plain, non-atomic instructions when **no two threads can ever update it concurrently**. In practice that holds in two situations: - **Thread confinement.** Every owning handle to the object lives in one thread and is never published anywhere another thread can reach — not into a shared collection, not into a queued task, not captured by a callback that runs elsewhere. - **External serialisation.** Every ownership change already happens while holding the same lock, so the lock's mutual exclusion is doing the job the atomic instruction would have done. Notice how strong the first condition is. It is not about *your* handle; it is about **every** handle to that object. A single copy handed to a background worker makes the object shared forever after, and every plain update anywhere in the program becomes a race. ## Why schemes offer two kinds of handle Because the tax is per ownership change and most objects are never shared, a counting scheme that only had the atomic form would make everyone pay for a guarantee most objects do not need. So schemes commonly expose two handle types with the same shape and different counts: | Handle kind | Count update | Requirement | Typical use | |---|---|---|---| | Thread-confined | plain load, add, store | no handle leaves the thread | local graphs, per-request structures | | Shared | atomic read-modify-write | none | objects published to other threads | The compiler or the type system may enforce the requirement, may merely document it, or a scheme may decide dynamically: some **bias** the count to one owning thread, letting that thread use plain updates while any other thread takes a slower atomic path, with a handshake that revokes the bias the first time an outsider appears. The handshake is the price, and it pays off only when the overwhelming majority of objects really are touched by one thread. ## The two failure directions When confinement is violated and two plain updates interleave, one of them is lost. Which one is lost decides the shape of the bug: 1. **A lost increment.** Two threads each take a handle; the count ends one lower than the number of live handles. Later the count reaches zero while a handle is still in use, the object is destroyed, and the surviving handle points at reclaimed memory. The symptom is a corrupted or nonsensical read, a crash inside unrelated code that reused the memory, or silent wrong answers. 2. **A lost decrement.** Two threads each drop a handle; the count ends one higher than reality and can never reach zero. The object is never destroyed and, with it, everything it transitively holds. The symptom is a slow footprint climb with no obvious owner. Both are **probabilistic**: they need two threads inside the same three-instruction window, which is rare enough that a test suite will not find it and a production node will. Both also surface **far from the cause** — the escape that made the object shared may be in a different module from the crash — which is why the requirement is usually enforced statically rather than debugged. ## Why you cannot just widen the count A common wrong instinct is that the problem is the count's *width* or *alignment*. It is neither. A lost update is not a torn write: each store is perfectly well-formed, and each thread writes a value that was correct when it read it. Widening the field, aligning it, or marking it as a single-word field changes nothing, because the hazard is the gap between the read and the write, not the write itself. The only fixes are to make the update indivisible or to make concurrency impossible. ## How this interacts with the shared-object tax This is the first and cheapest lever against counting cost, and it is the reason the cost is usually described as a tax on *shared* objects rather than on counting in general. An object that never escapes its thread pays almost nothing: plain updates to a line that the core already owns exclusively, with no ownership migration and no contention. The moment a handle is published, every update to that object's count — including all the ones made by the original thread — must become atomic, and the object joins the contended population. Designing so that objects stay confined is therefore not just a safety property; it is a throughput decision made at the point where a handle is published.
- Would making the count a wider field, or aligning it, remove the hazard?No. A lost update is not a torn or partially written value; every store is well formed. The hazard is the window between reading the old count and writing the new one, in which another thread can do the same. Width and alignment do not close that window; only indivisibility, or the absence of concurrency, does.
- If a thread-confined object is later published to another thread, what has to change?Every update to that object's count must become atomic from then on, including updates made by the original thread — a scheme cannot mix plain and atomic updates to the same count safely. In practice that means the object must be created with, or converted to, the shared handle kind before it is published, which is why publication points are the place to make the decision.
saying these in an interview costs you the question
- Thinks only the escaping handle needs atomic updates, not the original one
- Believes a wider or aligned count field removes the race
- Says the failure is a torn write rather than a lost update
- Assumes a lost update always leaks, never frees early
- Treats passing an object to a queued task as still thread-confined
- Expects tests to catch it, since the window is only a few instructions