布局

让我们从为我们的 Arc 实现创建布局开始。

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

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

不幸的是,这个引用计数本质上是共享的可变状态,所以 Arc 确实需要考虑同步。我们可以为此使用互斥锁,但这有点过头了。相反,我们将使用原子操作。而且由于每个人都需要指向 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,
}
}