use std::sync::atomic::{AtomicBool, AtomicI32, Ordering};
use std::thread;
fn main() {
let v = AtomicI32::new(0);
let flag = AtomicBool::new(false);
thread::scope(|s| {
s.spawn(|| {
while !flag.load(Ordering::Relaxed) { // #1
std::hint::spin_loop();
}
assert_eq!(v.swap(2, Ordering::Relaxed), 1); // #2
// assert_eq!(v.load(Ordering::Relaxed),1); // #5
});
s.spawn(|| {
if v.swap(1, Ordering::Relaxed) == 0 { // #3
flag.store(true, Ordering::Relaxed); // #4
}
});
});
}
In this example, the assert at #2 never fails. However, if you comment out #2 and uncomment #5, the assert at #5 can fail because #5 can either read 0 or 1.
Does it mean that an RMW is less prone to reading stale values than a pure load? So, for flag checking, we should use RMW operations rather than a pure load? If not that, what's the plausible reason here?
absolutely not. if you want ordering relations you should use the relevant atomic orderings and/or premade locks.
that being said, it is true that swap is less prone to reading "stale" values from a specific atomic in some sense. indeed the total modification order of v must be preserved, so in this specific case given #3 swapped v from 0 to 1, and #2 swapped v from something to 2, and v started at 0, it must necessarily be that what #2 swapped from 1.
in other words swap guarantees that if you only modify through swap, each value that is being swapped in will only be swapped out once.
you can think of it like if integers were !Copy, and swap was like core::mem::swap
a.) The execution order of the two spawned threads is arbitrary. The second thread can start, execute and finish before the first thread even started.
b.) There is no stale data. There is only Ordering. By using Relaxed you state that there is no ordering between these two atomics. So they can be stored and fetched at any time the compiler or CPU thinks will be fine.