Can `Arc::is_unique` return `true` in this example?

⚓ Rust    📅 2026-06-29    👤 surdeus    👁️ 2      

surdeus

Consider this example:

#![feature(arc_is_unique)]
use std::sync::{
    Arc,
    atomic::{AtomicBool, AtomicPtr, Ordering},
};
use std::thread;
fn main() {
    let mut is_unique = false;
    let arc = Arc::new(1); // #0
    let ptr = AtomicPtr::new(std::ptr::null_mut());
    let flag = AtomicBool::new(false);
    let ptr_rf = &ptr;
    let flag_rf = &flag;
    thread::scope(|s| {
        // thread 1
        s.spawn(move || {
            // clone() behaves like `strong.fetch_add(1,Relaxed)`
            let arc2 = arc.clone(); // #1
            ptr_rf.store(Arc::into_raw(arc2) as *mut i32, Ordering::Relaxed); // #2
            while !flag_rf.load(Ordering::Acquire) {} // #3
        });
        // thread 2
        s.spawn(|| {
            let raw_arc = loop {
                let val = ptr.load(Ordering::Relaxed) as *const i32; // #4
                if !val.is_null() {
                    break val;
                }
            };
            let arc = unsafe { Arc::from_raw(raw_arc as *const _) };
            is_unique = Arc::is_unique(&arc); // #5
            flag.store(true, Ordering::Release); // #6
        });
    });
    println!("{}", is_unique);
}

In this example, #0 creates the control block and initializes strong and weak to 1, respectively; these modifications all happens-before the beginning of the two threads. The clone() at #1, as explained in the comment, is a modification that increases strong by one.

Since #2 and #4 both use Relaxed memory ordering, the modification at #1 is unordered with #5; In other words, #1 doesn't happen before #5, the modification at #1 is not guaranteed to be visible to #5. According to the write-read coherence rule:

If a side effect X on an atomic object M happens before a value computation B of M, then the evaluation B takes its value from X or from a side effect Y that follows X in the modification order of M.

Such a side effect X in this example is the modification to strong at #0; this means the load to strong within Arc::is_unique is valid to read the initial value 1.

#6 synchronizing with #3 ensures that the load to strong within Arc::is_unique cannot read the modification of the drop of Arc in thread 1, which could be 1. This excludes the case that Arc::is_unqiue returns true because it reads that value.

So, is is_unique in this example possible to be true?

PS:

The implementation of Arc::is_unique is approximately as follows:

    pub fn is_unique(this: &Self) -> bool {
        // lock the weak pointer count if we appear to be the sole weak pointer
        // holder.
        //
        // The acquire label here ensures a happens-before relationship with any
        // writes to `strong` (in particular in `Weak::upgrade`) prior to decrements
        // of the `weak` count (via `Weak::drop`, which uses release). If the upgraded
        // weak ref was never dropped, the CAS here will fail so we do not care to synchronize.
        if this.inner().weak.compare_exchange(1, usize::MAX, Acquire, Relaxed).is_ok() {
            // This needs to be an `Acquire` to synchronize with the decrement of the `strong`
            // counter in `drop` -- the only access that happens when any but the last reference
            // is being dropped.
            let unique = this.inner().strong.load(Acquire) == 1;

            // The release write here synchronizes with a read in `downgrade`,
            // effectively preventing the above read of `strong` from happening
            // after the write.
            this.inner().weak.store(1, Release); // release the lock
            unique
        } else {
            false
        }
    }

2 posts - 2 participants

Read full topic

🏷️ Rust_feed