Rust Compare and Exchange
compare_exchange is an atomic operation that changes a value only if the value is still the one the caller saw. The check and the change happen as one indivisible step.
The shape: expect one value, swap in another
compare_exchange(expected, new, ...) asks: "is it still what I saw? then change it" — and reports the truth either way:
let result = counter.compare_exchange(10, 11, Ordering::SeqCst, Ordering::SeqCst);
// Ok(10) means: it was 10, and now it is 11
// Err(7) means: it was really 7 — nothing was changedBuild an add out of a retry loop
There is no fetch_add for every job. The standard shape is load, compute, then try the swap. On Err, start again from the real value it hands you:
use std::sync::atomic::{AtomicUsize, Ordering};
fn add_three(n: &AtomicUsize) {
let mut current = n.load(Ordering::SeqCst);
loop {
match n.compare_exchange(current, current + 3, Ordering::SeqCst, Ordering::SeqCst) {
Ok(_) => break,
Err(real) => current = real,
}
}
}
fn main() { let n = AtomicUsize::new(4); add_three(&n); add_three(&n); println!("n = {}", n.load(Ordering::SeqCst)); }The weak version may lose on purpose
compare_exchange_weak may report failure even when the value matched — a false alarm some hardware finds cheaper. Inside a retry loop that costs nothing, because the loop goes round again anyway:
// only inside a retry loop:
let result = flag.compare_exchange_weak(false, true, Ordering::Acquire, Ordering::Relaxed);A spinlock is this pattern, plus a store
The Rustonomicon builds a spinlock from exactly these pieces — grab with compare_exchange, let go with a Release store:
while lock.compare_exchange(false, true, Ordering::Acquire, Ordering::Relaxed).is_err() {
std::hint::spin_loop();
}
// ... the guarded work happens here ...
lock.store(false, Ordering::Release);Example
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::thread;
static MAX: AtomicUsize = AtomicUsize::new(0);
static CLAIMED: AtomicBool = AtomicBool::new(false);
static WINS: AtomicUsize = AtomicUsize::new(0);
fn raise_max(value: usize) {
let mut current = MAX.load(Ordering::SeqCst);
while value > current {
match MAX.compare_exchange(current, value, Ordering::SeqCst, Ordering::SeqCst) {
Ok(_) => break,
Err(real) => current = real,
}
}
}
fn main() {
let readings = [[12, 55, 9], [99, 3, 41], [73, 60, 28], [15, 88, 64]];
thread::scope(|s| {
for group in &readings {
s.spawn(move || {
for &r in group { raise_max(r); }
if CLAIMED.compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst).is_ok() {
WINS.fetch_add(1, Ordering::SeqCst);
}
});
}
});
println!("largest reading = {}", MAX.load(Ordering::SeqCst));
println!("winning claims = {}", WINS.load(Ordering::SeqCst));
}Key points
- compare_exchange(expected, new, success_order, failure_order) changes the value only if it still equals expected.
- It reports Ok(old) on success.
- It reports Err(actual) when another thread got there first.
- The failure ordering describes a read.
- So it may not be Release or AcqRel.
- SeqCst, Acquire and Relaxed are the legal choices there.
- Use the retry loop whenever the new value is computed from the old one.
- Maximums, conditional adds, and state machines in a single atomic word all fit this shape.
- Use compare_exchange_weak only inside a retry loop.
- Outside one, a false alarm would silently skip your update.
- This is the tool for jobs no fetch_ method covers: "add only while under 100", "claim this flag exactly once", or "raise the maximum".
- Under heavy contention the loop may spin many times before it wins.
- Any slow call inside it is paid again on every retry.
- A plain load, think, and store leaves a hole where another thread can change the value.
- A later store can then overwrite that thread's work.
In this example, four threads hold groups of sensor readings, and two compare_exchange patterns run at once. raise_max is a CAS loop. It loads the current maximum. If your reading beats it, it tries to swap it in. When another thread wins the race, Err hands you the real value. You retry from that. When the scope joins, MAX must hold 99. That is the largest reading in any group. This holds whichever order the threads happened to run in. The CLAIMED flag shows the one-winner pattern. Exactly one thread's compare_exchange(false, true) can ever succeed. Only the winner counts WINS, using fetch_add. So WINS prints exactly 1 on every run.
Note
Keep the work between load and swap tiny.
Try it Yourself
Edit the code, then press Run.
Running is simulated · Rust Playground
Exercise
compare_exchange returns Err(7) when you expected the value to be 10. What happened?