I'm not exactly sure what you mean but let me assume you meant one of these two meanings:
- Is the relaxed write only immediately visible to atomic reads within a single atomic instruction?
- Is the relaxed write only immediately visible to other atomic reads?
So to answer both:
- This is a bit of a weird question. Wouldn't the relevant kind of instruction be write-modify-read?
Anyway, not only.
- When you read non-atomically from a place written to by other threads and not synchronized by a happens-before it is a data race so yes.
I am so sorry for not looking at your initial query. Let me try to actually answer your question. That said, it might be a swell idea to go straight to reading Ch. 3..=6 of Rust Atomics and Locks by Mara Bos instead.
It is indeed fine to blindly use Relaxed memory ordering if only a single variable is involved. But when is only a single variable involved? Let's take your example: we need to deliver "please stop" to another thread.
trivial version:
let at_var = AtomicBool::new(true);
thread::scope(|s|
{
s.spawn(|| while at_var.load(Ordering::Relaxed) { /* stuff */ });
/* stuff */
at_var.store(false, Ordering::Relaxed);
});
And let's say both /* stuff */ blocks does not trigger undefined behavior and halts
, then this is totally fine, as at_var.load(Ordering::Relaxed) would eventually load at_var.store(false, Ordering::Relaxed).
But does the code in question really look like this? Often we write:
less trivial version:
let at_var = AtomicBool::new(true);
/* stuff */
thread::scope(|s|
{
s.spawn(||
{
while at_var.load(Ordering::Relaxed) { /* stuff */ }
/* stuff */
});
/* stuff */
at_var.store(false, Ordering::Relaxed);
});
Taking from yarn's example, this could look like:
let at_var = AtomicBool::new(true);
let ot_var = AtomicUsize::new(0);
thread::scope(|s|
{
s.spawn(||
{
while at_var.load(Ordering::Relaxed) {}
assert_eq!(ot_var.load(Ordering::Relaxed), 42);
});
ot_var.store(42, Ordering::Relaxed);
at_var.store(false, Ordering::Relaxed);
});
The assertion can actually fail! This is because the compiler has freedom to reorder the two relaxed atomic loads and the two relaxed atomic stores based on some separation assumptions, and out-of-order hacks done by a processor may be seen by another. So we need to ask both the compiler and the hardware to behave.
In rust, we do this by:
let at_var = AtomicBool::new(true);
let ot_var = AtomicUsize::new(0);
thread::scope(|s|
{
s.spawn(||
{
/* reads after this acq-load stay after it */
while at_var.load(Ordering::Acquire) {}
assert_eq!(ot_var.load(Ordering::Relaxed), 42);
});
ot_var.store(42, Ordering::Relaxed);
/* writes before this rel-store stay before it */
at_var.store(false, Ordering::Release);
});
Thus in this example, there's not only one variable involved in our delivery of the "please stop" signal. There's a separate payload 
So why would we go through the trouble of figuring out whether there's more than one variable involved, rather than going Acquire for reads, Release for writes and AcqRel for read-modify-write? Practically, that is because relaxed accesses preserve memory hierarchies for data accesses so hopefully less loads from unified memory are done than everything as AcqRel as possible.
big edit: apparently C11 (where Rust's atomics are from) may have restrictions on what "signal handlers" are [7.14.1..=2]: it might as well have nothing to do with preemption; The Rustonomicon also has an open issue about misconceptions with hardware causes of data racing so don't read it I guess 