布局

让我们从为 Arc 实现设计布局开始。

Arc<T> 为堆中分配的 T 类型的值提供线程安全的共享所有权。在 Rust 中,共享意味着不可变性,所以我们不需要设计任何东西来管理对该值的访问,对吧?虽然像 Mutex 这样的内部可变性类型允许 Arc 的用户创建共享可变性,但 Arc 本身不需要关心这些问题。

然而,Arc 确实需要关心一个地方的突变:销毁。当 Arc 的所有所有者都消失时,我们需要能够 drop 其内容并释放其分配。所以我们需要一种方法让所有者知道它是否是最后一个所有者,而最简单的方法是用所有者的计数——引用计数。

不幸的是,这个引用计数本质上是共享可变状态,所以 Arc 确实需要考虑同步。我们可以为此使用 Mutex,但这有点小题大做了。相反,我们将使用原子操作。而且由于每个人都需要一个指向 T 分配的指针,我们不妨将引用计数放在同一个分配中。

简单地说,它看起来像这样

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

pub struct Arc<T> {
    ptr: *mut ArcInner<T>,
}

pub struct ArcInner<T> {
    rc: atomic::AtomicUsize,
    data: T,
}
}

这可以编译,但这是不正确的。首先,编译器会给我们过于严格的变型。例如,Arc<&'static str> 不能用在需要 Arc<&'a str> 的地方。更重要的是,它会向 drop 检查器提供不正确的所属关系信息,因为它会假设我们不拥有任何 T 类型的值。由于这是一个提供值共享所有权的结构,因此在某些时候,该结构的实例将完全拥有其数据。有关变型和 drop 检查的所有详细信息,请参阅关于所有权和生命周期的章节

为了解决第一个问题,我们可以使用 NonNull<T>。请注意,NonNull<T> 是一个围绕原始指针的包装器,它声明

  • 我们对 T 是协变的
  • 我们的指针永远不会为空

为了解决第二个问题,我们可以包含一个包含 ArcInner<T>PhantomData 标记。这将告诉 drop 检查器,我们对 ArcInner<T>(它本身包含一些 T)的值有一定的所有权概念。

通过这些更改,我们得到了最终的结构

#![allow(unused)]
fn main() {
use std::marker::PhantomData;
use std::ptr::NonNull;
use std::sync::atomic::AtomicUsize;

pub struct Arc<T> {
    ptr: NonNull<ArcInner<T>>,
    phantom: PhantomData<ArcInner<T>>,
}

pub struct ArcInner<T> {
    rc: AtomicUsize,
    data: T,
}
}