I'm trying to understand few differences between std::sync::atomic::Atomic*
structs and primitives such as i32
, usize
, bool
in scope of multithreading.
First question, will another thread see changes to the non atomic type from another thread?
fn main() {
let mut counter = 0;
std::thread::scope(|scope| {
scope.spawn(|| counter = 1)
});
println!("{counter}");
}
Can I be sure that counter will be 1
right after another thread will write this value into it, or thread could cache this value? If not, will it work only with atomic type only?
fn main() {
let counter = AtomicI32::new(0);
std::thread::scope(|scope| {
scope.spawn(|| counter.store(1, Ordering::Release))
});
println!("{}", counter.load(Ordering::Acquire)); // Ordering::Acquire to prevent from reordering previous instructions
}
Second question, does Ordering
type affects when value in store
will be visible in other threads, or it will be visible right after store
, even if Ordering::Relaxed
was applied? As for example, will same code but with Ordering::Relaxed
and no instructions reorder show 1
in counter?
fn main() {
let counter = AtomicI32::new(0);
std::thread::scope(|scope| {
scope.spawn(|| counter.store(1, Ordering::Relaxed))
});
println!("{}", counter.load(Ordering::Relaxed));
}
I understand difference between atomic and non atomic writes to same variable, I'm only interested if another thread will see changes, even if this changes won't be consistent.
CodePudding user response:
First question, will another thread see changes to the non atomic type from another thread?
Yes. The difference between atomic and non-atomic variables is that you can change atomic variables using shared references, &AtomicX
, and not just using mutable references, &mut X
. This means that they can be changed in parallel in different threads. For primitives, the compiler will reject attempting that, e.g.:
fn main() {
let mut counter = 0;
std::thread::scope(|scope| {
scope.spawn(|| counter = 1);
scope.spawn(|| counter = 1);
});
println!("{counter}");
}
Or even the following, where we use the variable on the main thread but before the spawned thread is joined:
fn main() {
let mut counter = 0;
std::thread::scope(|scope| {
scope.spawn(|| counter = 1);
counter = 1;
});
println!("{counter}");
}
While with atomics this will work:
fn main() {
let counter = AtomicI32::new(0);
std::thread::scope(|scope| {
scope.spawn(|| counter.store(1, Ordering::Relaxed));
scope.spawn(|| counter.store(1, Ordering::Relaxed));
});
println!("{}", counter.load(Ordering::Relaxed));
}
Second question, does
Ordering
type affects when value in store will be visible in other threads, or it will be visible right after store, even ifOrdering::Relaxed
was applied? As for example, will same code but withOrdering::Relaxed
and no instructions reorder show 1 in counter?
No. Ordering
does not change what other threads will observe with this variable. And therefore, your usage of Release
and Acquire
is wrong.
On the other hand, Relaxed
here will suffice, for other reasons.
You are guaranteed to see the value 1 in your code no matter what ordering you will use, because std::thread::scope()
implicitly joins all spawned threads on exit, and joining a thread forms a happens-before relationship between everything done in this thread and the code after the join. In other words, you are guaranteed that everything done in the thread (and the includes storing to counter
) will happen before everything you do after you join it (and that includes reading counter
).
If there was not a join, for example, in this code:
fn main() {
let counter = AtomicI32::new(0);
std::thread::scope(|scope| {
scope.spawn(|| counter.store(1, Ordering::Release));
scope.spawn(|| println!("{}", counter.load(Ordering::Acquire)));
});
}
Then you are not guaranteed, despite the Release
and Acquire
orderings, to read the updated value. It may happen so, or may happen that you will read the old value.
Orderings are useful to create a happens-before relationship with different variables and code. But this is a complicated subject. I recommend reading this book (written by a Rust libs team member).