布局
让我们从为我们的 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, } }