克隆
现在我们已经设置了一些基本代码,我们需要一种克隆 Arc
的方法。
基本上,我们需要
- 递增原子引用计数
- 从内部指针构造一个新的
Arc
实例
首先,我们需要访问 ArcInner
let inner = unsafe { self.ptr.as_ref() };
我们可以如下更新原子引用计数
let old_rc = inner.rc.fetch_add(1, Ordering::???);
但是我们在这里应该使用什么排序呢?当克隆时,我们没有任何代码需要原子同步,因为我们在克隆时不会修改内部值。因此,我们可以使用 Relaxed 排序,这意味着没有 happens-before 关系,但它是原子的。然而,当 Drop
Arc 时,我们需要在递减引用计数时进行原子同步。这在 关于 Arc
的 Drop
实现的部分 中有更多描述。有关原子关系和 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
,这个假设是错误的。因此,当越来越多的 Arcs 从这个 Arc 中克隆出来,而它们没有被 Drop
并且引用计数没有被递减时,我们可能会溢出!这将导致 use-after-free,这是极其糟糕的!
为了处理这个问题,我们需要检查引用计数是否超过某个任意值(低于 usize::MAX
,因为我们将引用计数存储为 AtomicUsize
),并做一些事情。
标准库的实现决定直接中止程序(因为这在正常代码中是一种极不可能发生的情况,如果发生,程序可能极其退化),如果任何线程上的引用计数达到 isize::MAX
(大约是 usize::MAX
的一半),因为假设大概没有 20 亿个线程(或者在一些 64 位机器上大约有900 万亿个)同时递增引用计数。这就是我们将要做的。
实现此行为非常简单
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,
}
}
}