布局(Layout)
让我们从Arc实现的布局开始。
Arc<T>提供了在堆中分配的类型为T的值的线程安全共享所有权。 共享意味着Rust中的不变性,所以我们不需要设计任何东西来管理对值的访问,对吗? 虽然像 Mutex 这样的内部可变性类型允许 Arc 的用户创建共享可变性,但 Arc 本身不需要关心这些问题。
然而,有 一个地方 Arc 需要关注变化:破坏。 当 Arc 的所有所有者都离开时,我们需要能够drop其内容并释放其分配。 因此,我们需要一种让所有者知道它是否是 最后的 所有者的方法,而最简单的方法就是对所有者进行计数—引用计数。
不幸的是,这个引用计数本质上是共享的可变状态,所以 Arc 确实 需要考虑同步。 我们 可以 为此使用互斥锁,但这是多余的。 相反,我们将使用原子。 既然每个人都需要一个指向 T 的分配的指针,我们不妨将引用计数放在同一个分配中。
天真的,它看起来像这样:
use std::sync::atomic;pub struct Arc<T> {ptr: *mut ArcInner<T>,}pub struct ArcInner<T> {rc: atomic::AtomicUsize,data: T}
这可以编译,但是不正确的。 首先,编译器会给我们带来过于严格的变体。 例如,在需要Arc<&'a str>的地方,不能使用Arc<&'static str>。 更重要的是,它会向drop检查器提供不正确的所有权信息,因为它将假定我们不拥有类型T的任何值。因为这是一个提供值的共享所有权的结构,在某些时候会有完全拥有其数据的这种结构的一个实例。 这种的结构。 有关变体和drop检查的所有详细信息,请参阅关于所有权和生存期的章节。
为了解决第一个问题,我们可以使用NonNull<T>。 请注意,NonNull<T>是一个原始指针的包装,它声明以下内容:
我们是
T的变体我们的指针永远不会为空
为了解决第二个问题,我们可以包含一个包含ArcInner<T>的PhantomData标记。 这将告诉drop检查器我们有ArcInner<T>值(其本身包含T)的所有权概念。
经过这些更改,我们得到了最终的结构:
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}
