克隆

现在我们已经设置了一些基本代码,我们需要一种克隆 Arc 的方法。

基本上,我们需要

  1. 增加原子引用计数
  2. 从内部指针构造一个新的 Arc 实例

首先,我们需要访问 ArcInner

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

我们可以按如下方式更新原子引用计数

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

但是我们应该在这里使用什么顺序呢?我们在克隆时实际上没有任何代码需要原子同步,因为我们不会在克隆时修改内部值。因此,我们在这里可以使用 Relaxed 顺序,这意味着没有 happens-before 关系,但它是原子的。但是,在 Dropping Arc 时,我们需要在递减引用计数时进行原子同步。这在关于 ArcDrop 实现的部分中有更详细的描述。有关原子关系和 Relaxed 顺序的更多信息,请参阅关于原子操作的部分

因此,代码变成了这样

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

我们需要添加另一个导入来使用 Ordering

#![allow(unused)]
fn main() {
use std::sync::atomic::Ordering;
}

但是,我们现在的实现有一个问题。如果有人决定 mem::forget 一堆 Arcs 会怎样?到目前为止,我们编写的代码(以及将要编写的代码)都假设引用计数准确地描述了内存中有多少个 Arcs,但是使用 mem::forget 这是错误的。因此,当从这个 Arc 克隆出越来越多的 Arcs 并且它们没有被 Dropped 并且引用计数没有递减时,我们可能会溢出!这将导致 use-after-free,这是非常糟糕的!

为了处理这种情况,我们需要检查引用计数是否没有超过某个任意值(低于 usize::MAX,因为我们将引用计数存储为 AtomicUsize),并做一些事情

标准库的实现决定在任何线程上的引用计数达到 isize::MAX(大约是 usize::MAX 的一半)时中止程序(因为这在正常代码中是极不可能发生的情况,如果发生了,程序可能已经非常退化了),假设可能没有大约 20 亿个线程(或者在某些 64 位机器上大约是9 万亿)同时增加引用计数。这就是我们要做的。

实现这种行为非常简单

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

然后,我们需要返回一个新的 Arc 实例

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

现在,让我们把所有这些都包装在 Clone 实现中

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,
        }
    }
}