克隆
现在我们已经有了一些基本的代码,我们需要一种方法来克隆Arc
。
我们大致需要:
- 递增原子引用计数
- 从内部指针构建一个新的
Arc
实例
首先,我们需要获得对ArcInner
的访问。
let inner = unsafe { self.ptr.as_ref() };
我们可以通过以下方式更新原子引用计数:
let old_rc = inner.rc.fetch_add(1, Ordering::???);
但是我们在这里应该使用什么顺序?我们实际上没有任何代码在克隆时需要原子同步,因为我们在克隆时不修改内部值。因此,我们可以在这里使用 Relaxed 顺序,这意味着没有 happen-before 的关系,但却是原子性的。然而,当Drop
Arc 时,我们需要在递减引用计数时进行原子同步。这在关于Arc
的Drop
实现部分中有更多描述。关于原子关系和 Relaxed ordering 的更多信息,请参见atomics 部分。
因此,代码变成了这样:
let old_rc = inner.rc.fetch_add(1, Ordering::Relaxed);
我们需要增加一个导入来使用Ordering
。
#![allow(unused)] fn main() { 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万亿个)在同时增加引用计数。这就是我们要做的。
实现这种行为是非常简单的。
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() };
// 我们没有修改 Arc 中的数据,因此在这里不需要任何原子的同步操作,
// 使用 relax 这种排序方式也就完全可行了
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,
}
}
}