[Day39] Read Rust Atomics and Locks - Upgrade Arc with Weak
ability
by Mara Bos
At Topic: Chapter 6. Weak Pointers
A Weak<T>
aka. weak pointer provides a solution for dropping "cyclic structure"
Recall
Arc
- Goal: To share ownership
- Thread safe (While
Rc
is not) - Immutable (So is
Rc
)
let a = Arc::new([1, 2, 3]); let b = a.clone(); assert_eq!(a.as_ptr(), b.as_ptr()); // Same allocation!
UnsafeCell
- An
UnsafeCell
is the primitive building unit for interior mutability - No restrictions to avoid undefined behavior
- Can only be used in
unsafe
block - Commonly, an
UnsafeCell
is wrapped in another type that provides safety through a limited interface, such asCell
orMutex
. - In other words, all types with interior mutability are built on top of
UnsafeCell
, Including:Cell
,RefCell
,RwLock
,Mutex
... - Gets a mutable raw pointer (
*mut T
) to the wrapped value byget
method.
pub const fn get(&self) -> *mut T
Compare-and-Exchange Operations
- Compare-and-Exchange checks if the atomic value is equal to a given value, and only if that is the case does it replace it with a new value (as a single operation)
compare_exchange_weak
: Similar tocompare_exchange
, but the difference is that the weak version may still sometimes leave the value untouched and return an Err, even though the atomic value matched the expected value.- We could use Compare-and-Exchange to implement all the other atomic operations by putting it in a loop to retry if it did change
// source: https://doc.rust-lang.org/std/sync/atomic/struct.AtomicUsize.html#examples-11 use std::sync::atomic::{AtomicUsize, Ordering}; let some_var = AtomicUsize::new(5); // some_var compares to the first argument (5) and if it is equal, it will be replaced by the second argument (10) assert_eq!(some_var.compare_exchange(5, 10, Ordering::Acquire, Ordering::Relaxed), Ok(5)); assert_eq!(some_var.load(Ordering::Relaxed), 10);
Notes
To see full implementation, please go here. I just write down things that I feel important.
Weak<T>
behaves similar toArc<T>
, but does not prevent an object from getting dropped =>Weak<T>
can exist without aT
Weak<T>
can be upgraded toArc<T>
through itsupgrade
method, but only if theT
still exists.- To provide
Arc
as well asWeak
, it is needed to modifyArcData
struct:
// Before struct ArcData<T> { ref_count: AtomicUsize, data: T, } // After struct ArcData<T> { /// Number of `Arc`s. data_ref_count: AtomicUsize, /// Number of `Arc`s and `Weak`s combined. alloc_ref_count: AtomicUsize, /// The data. `None` if there's only weak pointers left. data: UnsafeCell<Option<T>>, }
- When implementing
Deref
forArc
, note that the type ofArcData.data
is nowUnsafeCell<Option<T>>
rather thanT
. Therefore, it takes more steps to get&T
:
// Instead of fn deref(&self) -> &T { &self.data().data } // It should be fn deref(&self) -> &T { // Using `UnsafeCell::get()` let ptr = self.weak.data().data.get(); // Safty: Since there's an Arc to the data, // the data exists and may be shared. // (*ptr): *mut Option<T> => Option<T> // (*ptr).as_ref(): &Option<T> to Option<&T> unsafe { (*ptr).as_ref().unwrap() } }
- There are 2 focal points about
Arc::get_mut
:
struct ArcData<T> { data_ref_count: AtomicUsize, alloc_ref_count: AtomicUsize, // key for checking if it's the only Arc data: UnsafeCell<Option<T>>, } impl<T> Arc<T> { pub fn get_mut(arc: &mut Self) -> Option<&mut T> { if arc.weak.data().alloc_ref_count.load(Relaxed) == 1 { fence(Acquire); // Safety: Nothing else can access the data, since // there's only one Arc, to which we have exclusive access, // and no Weak pointers. let arcdata: &mut ArcData<T> = unsafe { arc.weak.ptr.as_mut() }; let option: &mut Option<T> = arcdata.data.get_mut(); // We know the data is still available since we // have an Arc to it, so this won't panic. let data: &mut T = option.as_mut().unwrap(); Some(data) } else { None } } }
- By
alloc_ref_count
, we can check bothArc
andWeak
count at the same time. Ifalloc_ref_count
is 1, it means there's only oneArc
and noWeak
pointers. So, it's safe to get mutable access to the data. - (My guess)
arc.weak.ptr.as_mut()
is used rather thanarc.weak.data()
because we need&mut ArcData<T>
rather than&ArcData<T>
.
- (Quote from the book) Dropping an object in Rust will first run its Drop::drop function (if it implements Drop), and then drop all of its fields, one by one, recursively.
Questions
Why does not Weak
implemented Deref
?
According to Claude
- Safety considerations: Weak pointers don't guarantee that the data they point to still exists. If
Weak
implementedDeref
, users might incorrectly assume that the data is always available, which could lead to unsafe behavior. - Semantic mismatch:
Deref
is typically used to represent some kind of "ownership" or "guaranteed existence" relationship. Weak pointers fundamentally don't provide this guarantee. - Different usage patterns:
Arc
needs frequent access to its internal data, soDeref
is useful.Weak
is primarily used to check if data exists and potentially upgrade to anArc
, not for direct access.
My Difficulty
- I know
UnsafeCell
supplies more control and lower overhead and is great for synchronization. But the timing to use it rather thanCell
orRefCell
is quite difficult! - When to know my customized struct needs to implement
Drop
?