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