Please explain Acquire/Release to me. I have read the definitions and examples many times, but I couldn't understand one thing - guarantees.
What I understood 100% is that it has nothing to do with thread or instruction locks. Atomics blocks threads at the instruction level, and there are also system locks at the kernel level to block threads explicitly.
And what this has to do with is the barriers and ordering of memory accesses. These accesses may be executed chaotically by default, so we can order them with Acquire/Release.
For example, the following code:
pub fn change(&self, f: impl FnOnce(&mut T) -> T) {
let load_data = self.ptr.load(Acquire);
let changed_data = f(&mut unsafe { &*load_data }.clone());
self.ptr.store(Box::into_raw(Box::new(changed_data)), Release);
}
The change function loads the old data, changes it, and then saves the changed data (implied atomically).
Based on this code, I will explain my problem.
So, I have a perhaps false impression about persistence: When the first thread executes acquire-load
, it means that the parallel thread that wants to execute the same acquire-load
will see that the data in memory is already locked (supposedly the barrier means just that) and will wait for it to be unlocked (that very waiting) until the data is released by some Release
.
But understandably this is not the case. If it were so, if Release
didn't happen, the second thread would wait forever, which would look strange. And what happens is only what is called guarantees, and it does not affect thread locking in any way, neither through memory, nor through instructions, nor in any other way.
The guarantees that are given in this code are only: if the second thread encounters changed data by another thread after change-store-Release
, it means that the 2 lines before it 100% happened, i.e. the other thread 100% processed them.
But because of the misconception and uncertainty I can't fully accept: does this mean that a parallel thread has the possibility to change self.ptr.store
before the first thread, thus causing a data race (inter-thread), which may end up giving irrelevant data. And Acquire/Release
has no effect on this, all they will guarantee is "if another thread sees self.ptr.store
from the second thread, the 2 lines before it happened 100%", which is not important compared to atomic locks.
That is, I still have to check the data when I try to do store
, because in one thread between load
and this store
(which happened in strict order, thanks to our unimportant guarantees) another thread could have already changed the atomic variable in parallel.
That is, it still needs to:
pub fn change(&self, f: impl Fn(&mut T) -> T) {
let mut load_data = self.ptr.load(Acquire);
loop {
let changed_data = f(&mut unsafe { &*load_data }.clone());
match self.ptr.compare_exchange(load_data, Box::into_raw(Box::new(changed_data)), Release, Relaxed) {
Ok(_) => break,
Err(e) => load_data = e,
}
}
}
to ensure that the data is relevant.