In this lazy initialization example, the author uses Acquire
ordering to load the value or PTR
, in line A.
use std::sync::atomic::AtomicPtr;
fn get_data() -> &'static Data {
static PTR: AtomicPtr<Data> = AtomicPtr::new(std::ptr::null_mut());
let mut p = PTR.load(Acquire); // <---- line A
if p.is_null() {
p = Box::into_raw(Box::new(generate_data()));
if let Err(e) = PTR.compare_exchange(
std::ptr::null_mut(), p, Release, Acquire
) {
// Safety: p comes from Box::into_raw right above,
// and wasn't shared with any other thread.
drop(unsafe { Box::from_raw(p) });
p = e;
}
}
// Safety: p is not null and points to a properly initialized value.
unsafe { &*p }
}
She later explains the use of Acquire
is to prevent the compiler from reordering initialization of the data with store operation.
This is the original text:
If we leave the
strict memory model aside and think of it in more practical terms, we could say that
the release ordering prevents the initialization of the data from being reordered with
the store operation that shares the pointer with the other threads. This is important,
since otherwise other threads might be able to see the data before it’s fully initialized.
From my understanding, the author tries to avoid this reordering:
// ...
// Now p may be null
let mut p = PTR.load(Acquire); // <---- line A
if p.is_null() {
if let Err(e) = PTR.compare_exchange(
std::ptr::null_mut(), p, Release, Acquire
) {
// ...
}
// This line is executed after PTR.compare_exchange now
p = Box::into_raw(Box::new(generate_data()));
// ...
}
But how is it possible? This line
p = Box::into_raw(Box::new(generate_data()));
does not access any variables shared between threads, and it has a happen-before relationship with the following because they are in the same thread.
PTR.compare_exchange(std::ptr::null_mut(), p, Release, Acquire)
As the author says,
The basic happens-before rule is that everything that happens within the same thread
happens in order. If a thread is executing f(); g();, then f() happens-before g().
I can only persuade myself with these two reasonings:
- Although
p = Box::into_raw(Box::new(generate_data()));
has a happen-before relationship withPTR.compare_exchange(std::ptr::null_mut(), p, Release, Acquire)
in the same thread, other threads do not have this relationship and so may see the reverted order. This is weired, as from the thread that callsPTR.compare_exchange(std::ptr::null_mut(), p, Release, Acquire)
,PTR
is set to point to the initialized data, no matter how other threads see it,PTR
is guaranteed to be non-null ifPTR.compare_exchange(std::ptr::null_mut(), p, Release, Acquire)
succeeds. - The acquire-load (
let mut p = PTR.load(Acquire);
) and release-store (PTR.compare_exchange(std::ptr::null_mut(), p, Release, Acquire)
) pairing just improves correctness and prevents the compiler from making incorrect assumptions and reorder the instructions somehow.