You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
nomicon/src/arc-clone.md

3.2 KiB

Cloning

Now that we've got some basic code set up, we'll need a way to clone the Arc.

Basically, we need to:

  1. Increment the atomic reference count
  2. Construct a new instance of the Arc from the inner pointer

First, we need to get access to the ArcInner:

let inner = unsafe { self.ptr.as_ref() };

We can update the atomic reference count as follows:

let old_rc = inner.rc.fetch_add(1, Ordering::???);

But what ordering should we use here? We don't really have any code that will need atomic synchronization when cloning, as we do not modify the internal value while cloning. Thus, we can use a Relaxed ordering here, which implies no happens-before relationship but is atomic. When Dropping the Arc, however, we'll need to atomically synchronize when decrementing the reference count. This is described more in the section on the Drop implementation for Arc. For more information on atomic relationships and Relaxed ordering, see the section on atomics.

Thus, the code becomes this:

let old_rc = inner.rc.fetch_add(1, Ordering::Relaxed);

We'll need to add another import to use Ordering:

use std::sync::atomic::Ordering;

However, we have one problem with this implementation right now. What if someone decides to mem::forget a bunch of Arcs? The code we have written so far (and will write) assumes that the reference count accurately portrays how many Arcs are in memory, but with mem::forget this is false. Thus, when more and more Arcs are cloned from this one without them being Dropped and the reference count being decremented, we can overflow! This will cause use-after-free which is INCREDIBLY BAD!

To handle this, we need to check that the reference count does not go over some arbitrary value (below usize::MAX, as we're storing the reference count as an AtomicUsize), and do something.

The standard library's implementation decides to just abort the program (as it is an incredibly unlikely case in normal code and if it happens, the program is probably incredibly degenerate) if the reference count reaches isize::MAX (about half of usize::MAX) on any thread, on the assumption that there are probably not about 2 billion threads (or about 9 quintillion on some 64-bit machines) incrementing the reference count at once. This is what we'll do.

It's pretty simple to implement this behaviour:

if old_rc >= isize::MAX as usize {
    std::process::abort();
}

Then, we need to return a new instance of the Arc:

Self {
    ptr: self.ptr,
    phantom: PhantomData
}

Now, let's wrap this all up inside the Clone implementation:

use std::sync::atomic::Ordering;

impl<T> Clone for Arc<T> {
    fn clone(&self) -> Arc<T> {
        let inner = unsafe { self.ptr.as_ref() };
        // Using a relaxed ordering is alright here as we don't need any atomic
        // synchronization here as we're not modifying or accessing the inner
        // data.
        let old_rc = inner.rc.fetch_add(1, Ordering::Relaxed);

        if old_rc >= isize::MAX as usize {
            std::process::abort();
        }

        Self {
            ptr: self.ptr,
            phantom: PhantomData,
        }
    }
}