克隆

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

我们大致需要:

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

首先,我们需要获得对ArcInner的访问。

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

我们可以通过以下方式更新原子引用计数:

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

但是我们在这里应该使用什么顺序?我们实际上没有任何代码在克隆时需要原子同步,因为我们在克隆时不修改内部值。因此,我们可以在这里使用 Relaxed 顺序,这意味着没有 happen-before 的关系,但却是原子性的。然而,当Drop Arc 时,我们需要在递减引用计数时进行原子同步。这在关于ArcDrop实现部分中有更多描述。关于原子关系和 Relaxed ordering 的更多信息,请参见atomics 部分

因此,代码变成了这样:

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

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

  1. use std::sync::atomic::Ordering;

然而,我们现在的这个实现有一个问题:如果有人决定mem::forget一堆 Arc 怎么办?到目前为止,我们所写的代码(以及将要写的代码)假设引用计数准确地描绘了内存中的 Arc 的数量,但在mem::forget的情况下,这是错误的。因此,当越来越多的 Arc 从这个 Arc 中克隆出来,而它们又没有被Drop和参考计数被递减时,我们就会溢出!这将导致释放后使用(use-after-free)。这是非常糟糕的事情!

为了处理这个问题,我们需要检查引用计数是否超过某个任意值(低于usize::MAX,因为我们把引用计数存储为AtomicUsize),并做一些防御

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

实现这种行为是非常简单的。

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

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

  1. Self {
  2. ptr: self.ptr,
  3. phantom: PhantomData
  4. }

现在,让我们把这一切包在Clone的实现中。

  1. use std::sync::atomic::Ordering;
  2. impl<T> Clone for Arc<T> {
  3. fn clone(&self) -> Arc<T> {
  4. let inner = unsafe { self.ptr.as_ref() };
  5. // 我们没有修改 Arc 中的数据,因此在这里不需要任何原子的同步操作,
  6. // 使用 relax 这种排序方式也就完全可行了
  7. let old_rc = inner.rc.fetch_add(1, Ordering::Relaxed);
  8. if old_rc >= isize::MAX as usize {
  9. std::process::abort();
  10. }
  11. Self {
  12. ptr: self.ptr,
  13. phantom: PhantomData,
  14. }
  15. }
  16. }