Mehdi Akiki
Rust Failure Atlas / Runtime, memory, and library APIs

RFA-687 · Case file with fixtures · Case 659 of 694 · Runtime evidence

compare_exchange Failure Returns the Actual Atomic Value

CAS failure reports the value actually observed. Retry loops must update expectations, recompute dependent state, and use justified success and failure orderings.

Reviewed
Rust
Rust 1.98.1, edition 2024
Targets
targets supporting AtomicUsize
Profiles
dev, release, test

Direct answer

What this Rust failure means

Why it happens
CAS failure reports the newly observed actual state so a caller can reconsider its transition instead of returning the already-known expectation.
First discriminating check
Update the observation, recompute desired state, and justify both memory orderings and retry progress before attempting again.

compare_exchange(current, new, success, failure) writes new only if the atomic equals current. On mismatch, Err contains the actual value observed from the atomic. The failing fixture expects its stale proposal four back and receives five.

Err is fresh evidence, not the rejected argument

The compare_exchange contract returns Ok(previous) after a successful exchange and Err(actual) after failure. The caller already knows the expected value it supplied; the useful information is what memory contained.

This supports retry loops. They can update the expected value, recompute a desired transition, and attempt again. The repaired fixture verifies Err(5) and then succeeds with expectation five.

Recompute before retrying

If the new value depends on the old one, blindly changing only the expected operand can install a result calculated from stale state. A counter increment loop recomputes observed + 1 each time. A state machine checks whether the newly observed state still permits the transition.

Contention can cause many retries. Backoff or a lock may outperform a hot compare-exchange loop. I benchmark the actual workload and preserve a bounded progress story.

fetch_update can express some update loops while still requiring a closure that handles newly observed values.

Memory ordering has two paths

Compare-exchange accepts success and failure orderings because the successful read-modify-write and failed load have different semantics. The failure ordering cannot include Release or AcqRel because no write occurred.

I derive ordering from a documented happens-before relationship, not from a desire to make the code “strong enough.” SeqCst simplifies the fixture but does not explain a production protocol. Relaxed can be sufficient for independent counters and insufficient for publishing associated data.

The Ordering documentation is necessary, but an algorithm-level proof is still required.

ABA is not detected by equal values

CAS compares the current bits. A location can change from A to B and back to A, making the comparison succeed even though state changed in between. This ABA problem matters for pointers, freelists, and versioned state.

Generation tags, hazard pointers, epoch reclamation, or a lock may be required. Atomic pointer equality also does not prove an allocation is still valid without a safe reclamation scheme.

I do not build lock-free ownership from integer examples alone. Memory reclamation is usually the hardest invariant.

Weak compare exchange permits spurious failure

compare_exchange_weak may fail spuriously even when values match, which can be efficient inside loops on some platforms. Code must already retry and treat every Err as an observation, not a logical conflict verdict.

The strong form is often clearer for a one-shot attempt. Neither form makes the wider multi-field operation atomic.

Metrics around CAS track retries and contention, not only final success. A system can be correct but spend excessive CPU repeatedly losing the same cache line.

Tests separate state and ordering proof

The deterministic fixture creates a known mismatch and asserts returned current state. Concurrent stress tests exercise allowed transitions and final invariants, but passing them does not prove memory ordering.

Tools and formal models can help with subtle interleavings. I keep the protocol small enough to explain in comments and code review.

I also avoid packing unrelated fields into one integer only to claim lock freedom. Bit allocation, rollover, and atomic width support become new contracts. A clear mutex-protected struct can be faster under low contention and much easier to evolve. Atomics earn their place when measurement and a reviewable correctness argument support them.

My compare-exchange checklist

  • Does Err get interpreted as the actual observed value?
  • Is the desired state recomputed after every mismatch?
  • Can the retry loop starve or create cache-line contention?
  • What happens-before edges justify both orderings?
  • Could ABA make equal bits hide an intervening change?
  • Is memory reclamation safe for atomic pointers?
  • Is weak CAS used only in a retry-capable path?
  • Would a lock make the invariant clearer at acceptable cost?

The core principle is that CAS is a conditional state observation plus update. Failure returns new evidence about shared state. I use that evidence to reconsider the transition rather than mechanically repeating a stale calculation.