布局(Layout)

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

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

然而, 一个地方 Arc 需要关注变化:破坏。 当 Arc 的所有所有者都离开时,我们需要能够drop其内容并释放其分配。 因此,我们需要一种让所有者知道它是否是 最后的 所有者的方法,而最简单的方法就是对所有者进行计数—引用计数。

不幸的是,这个引用计数本质上是共享的可变状态,所以 Arc 确实 需要考虑同步。 我们 可以 为此使用互斥锁,但这是多余的。 相反,我们将使用原子。 既然每个人都需要一个指向 T 的分配的指针,我们不妨将引用计数放在同一个分配中。

天真的,它看起来像这样:

  1. use std::sync::atomic;
  2. pub struct Arc<T> {
  3. ptr: *mut ArcInner<T>,
  4. }
  5. pub struct ArcInner<T> {
  6. rc: atomic::AtomicUsize,
  7. data: T
  8. }

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

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

  • 我们是T的变体

  • 我们的指针永远不会为空

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

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

  1. use std::marker::PhantomData;
  2. use std::ptr::NonNull;
  3. use std::sync::atomic::AtomicUsize;
  4. pub struct Arc<T> {
  5. ptr: NonNull<ArcInner<T>>,
  6. phantom: PhantomData<ArcInner<T>>
  7. }
  8. pub struct ArcInner<T> {
  9. rc: AtomicUsize,
  10. data: T
  11. }