Confusion with this Acquire fence

Someone also asked this in rust lang discord server but it was a bit unclear to me...

Can someone please explain how this fence helps to make sure that nothing is accessing the shared allocation? It's a bit confusing because with Release and Acquire ordering one should be loading the Released value to make sure that an happens-before relationship was established...

Does this fence apply Acquire ordering to the fetching part of fetch_sub when the counter goes from 0 to 1?

impl<T> Drop for Arc<T> {
    fn drop(&mut self) {
        if self.data().ref_count.fetch_sub(1, Release) == 1 {
            fence(Acquire);
            unsafe {
                drop(Box::from_raw(self.ptr.as_ptr()));
            }
        }
    }
}

this piece of code is from the atomics and locks book: Rust Atomics and Locks — Chapter 6. Building Our Own "Arc"

It doesn't. Only the Release does so.

It's going to 0. (I assume you intended that.) (No code will ever read 0 though.)
The data could have been modified in another thread (such as one what called drop before so setting counter to 1.)
The Acquire ensures this thread sees the same data as the data modifying thread (that set counter to 1.)

Yeah i meant 1 to 0...

The Acquire ordering is applied to the ref_count variable is that correct?

Also, is this correct in this case?

The thread has already read the ref_count as being 1 and set it to 0 at point of fence call. It is other memory that has been associated with the ref_count being set to 1 by Release that this thread is now allowed rely upon.

Atomics are confusing...! I think about it like this:

Release means roughly "I have finished writing to shared memory, flush my local cache to global memory".

Acquire means roughly "I am going to be reading shared memory, be sure to reload the local cache from global memory".

In my understanding, fence synchronizes with all previous fetch_sub(1, Release)s. In other words, if another thread has modified the guarded data, fence makes sure that we will see those changes (which is needed for correct execution of Drop). Note that we do not need to synchronize with changes performed by the current thread.

We could've used fetch_sub(1, AcqRel) instead without any fence, but it would mean that Acquire synchronization is performed on each decrement, which is somewhat wasteful since we only need it when the counter reaches 0.

So it's just a micro-optimization for targets with weak memory model.

An important caveat is that with atomic operations (as opposed to fences) synchronize only when Acquire/Release pair is executed on the same memory, i.e. a.store(Release) and b.load(Acquire) do not synchronize with each other if a and b point to different atomics. It's better to think about atomics in terms of "happened-before" relationships and potential memory operation reorderings.

To prove the call to drop(Box::from_raw(self.ptr.as_ptr())) correct, you must prove that all usage of the data happens-before dropping the data.

Let's say we have two threads:

// thread A
use(my_arc);
drop(my_arc) {
    refcount.fetch_sub(1, Release); // X: 2 -> 1
// thread B
drop(my_arc) {
    refcount.fetch_sub(1, Release); // Y: 1 -> 0
    fence(Acquire); // F
    drop(Box::from_raw(ptr));
}

so in this scenario we must be able to prove that use(my_arc) happens-before drop(Box::from_raw(ptr)). We can do so using the rules for atomic-fence synchronization.

An atomic release operation X in thread A synchronizes-with an acquire fence F in thread B, if

  • there exists an atomic read Y (with any memory order),
  • Y reads the value written by X,
  • Y is sequenced-before F in thread B.

Let's say that X is the fetch_sub in thread A. Then the conditions are satisfied:

  1. There is an atomic read Y, namely the fetch_sub in thread 2 that goes from 1 -> 0.
  2. X read 2 then wrote 1, and Y read 1, so Y read the value written by X.
  3. Y is sequenced-before the acquire fence because they're right after each other on the same thread.

We can therefore conclude that X synchronizes-with F. Thus, use(my_arc) happens-before X happens-before F happens-before drop(Box::from_raw(ptr)).

thanks a lot to everyone! I now understand how exactly it works.