We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← Atomics, Send/Sync and the Memory Model step 2 of 12
CAS loops: compare_exchange and fetch_update
Compute (∏ values) mod m into a shared AtomicU64, from several threads.
pub fn atomic_product_mod(values: Vec<u64>, m: u64, workers: usize) -> u64
Empty input gives 1 % m. m == 0 returns 0. Treat workers == 0 as 1.
The tests run workers = 1, 2, 8 and demand identical answers.
Use u128 for the intermediate. values may contain numbers near
u64::MAX, and a * b in u64 would overflow — silently, because this build
is -O and overflow checks are off. (u128::from(a) * u128::from(b)) % m is
exact.
Modular multiplication is commutative and associative, so the product is the same whatever order the threads apply their factors. That is what makes a genuinely racy computation deterministically testable.
There is no fetch_mul, and there never will be
The starter tries one and gets E0599. Look at what the atomic API actually
offers: fetch_add, fetch_sub, fetch_and, fetch_or, fetch_xor,
fetch_min, fetch_max, swap. That is not a curated selection — it is
the list of read-modify-write operations the hardware implements as single
instructions.
Everything else you build yourself, out of the one universal primitive:
fn compare_exchange(&self, current: T, new: T, success: Ordering, failure: Ordering)
-> Result<T, T>
“If the value is still current, replace it with new and return Ok(old).
Otherwise change nothing and return Err(actual).” Atomically.
The retry loop
let mut cur = acc.load(Relaxed);
loop {
let next = f(cur);
match acc.compare_exchange_weak(cur, next, Relaxed, Relaxed) {
Ok(_) => break,
Err(actual) => cur = actual, // someone beat us — recompute from theirs
}
}
Read it as an optimistic transaction: compute assuming nothing changed, then commit only if nothing did. If it did, you did not corrupt anything — you wasted one iteration and try again from the value that actually won.
This is what “lock-free” means. Not “no synchronisation” — there is plenty. It means no thread can block the others: whoever loses a CAS retries, and the winner always makes progress, so the system as a whole cannot stall because one thread was descheduled mid-update. A mutex holder that gets descheduled stops everybody.
It also means CAS loops can starve an individual thread under heavy contention. Lock-free is a system-level guarantee, not a per-thread one.
_weak versus strong, and why _weak here
compare_exchange_weak may fail spuriously — return Err even when the
value did match.
That sounds like a defect and is a deliberate hardware concession. On ARM and RISC-V, CAS is compiled from a load-linked/store-conditional pair, and the store-conditional can fail for reasons that have nothing to do with your value — a context switch, an interrupt, another core touching the same cache line. The strong version must therefore add its own retry loop internally to hide those failures.
So: inside a retry loop you want _weak, because you are going to loop
anyway and the extra internal loop is pure waste. Use the strong form only for
one-shot attempts, where “did I win?” is the answer you need.
This is also the classic intermittent-failure bug: a solution that calls
compare_exchange_weak(...).unwrap() works on x86 (where the spurious failure
mode barely exists) and fails at random on ARM. Handle the Err — that is
what the loop is.
One rule the compiler enforces: the failure ordering may not be stronger
than the success ordering. rustc’s invalid_atomic_ordering lint is
deny-by-default and catches the syntactic misuses for free.
fetch_update, the same loop with a nicer face
acc.fetch_update(Relaxed, Relaxed, |cur| Some(mul_mod(cur, v, m))).ok();
It writes the retry loop for you, using _weak internally, and lets the
closure return None to abandon the update. Prefer it in real code. Write the
explicit loop at least once first, so that you know what it is doing when it
shows up in a profile.
::: question Why does Relaxed suffice here, when this is a read-modify-write cycle?
Because the accumulator does not publish anything.
Ordering exists to make other memory visible: a Release store followed by
an Acquire load is how one thread says “the buffer I filled before this
store is now safe to read”. Here there is no buffer. The only thing any thread
learns from the atomic is the atomic’s own value, and Relaxed already
guarantees each variable has a single total modification order that everyone
agrees on.
The CAS itself is still atomic under Relaxed — atomicity and ordering are
independent properties, and confusing them is the most common misreading of
the memory model. Relaxed gives up ordering, never atomicity.
The moment your CAS publishes a pointer to data written beforehand, the
success ordering must become AcqRel and the readers’ load Acquire. The
next article is about exactly where that line is, and about why this grader
cannot test which side of it you are on.
:::
Loading visualization…
Stuck?
Rust reference solution
Sign in to attempt this problem and reveal the reference solution.