丢弃

我们现在需要一种方法来减少引用计数,并在计数足够低时丢弃数据,否则数据将永远存在于堆中。

为了做到这一点,我们可以实现Drop

我们大致需要:

  1. 递减引用计数
  2. 如果数据只剩下一个引用,那么:
  3. 原子化地对数据进行屏障,以防止对数据的使用和删除进行重新排序
  4. 丢弃内部数据

首先,我们需要获得对ArcInner的访问:

let inner = unsafe { self.ptr.as_ref() };

现在,我们需要递减引用计数。为了简化我们的代码,如果从fetch_sub返回的值(递减引用计数之前的值)不等于1,我们可以直接返回(我们不是数据的最后一个引用)。

if inner.rc.fetch_sub(1, Ordering::Release) != 1 {
    return;
}

然后我们需要创建一个原子屏障来防止重新排序使用数据和删除数据。正如标准库对Arc的实现中所述。

需要这个内存屏障来防止数据使用的重新排序和数据的删除。因为它被标记为Release,引用计数的减少与Acquire屏障同步。这意味着数据的使用发生在减少引用计数之前,而减少引用计数发生在这个屏障之前,而屏障发生在数据的删除之前。(译者注:use < decrease < 屏障 < delete)

正如Boost 文档中所解释的那样。

强制要求一个线程中对该对象的任何可能的访问(通过现有的引用)发生在不同线程中删除该对象之前是很重要的。这可以通过在丢弃一个引用后的“Release”操作来实现(任何通过该引用对对象的访问显然必须在之前发生),以及在删除对象前的“Acquire”操作。

特别是,虽然 Arc 的内容通常是不可改变的,但有可能对类似 Mutex 的东西进行内部可变。由于 Mutex 在被删除时不会被获取,我们不能依靠它的同步逻辑来使线程 A 的写操作对线程 B 的析构器可见。

还要注意的是,这里的 Acquire fence 可能可以用 Acquire load 来代替,这可以在高度竞争的情况下提高性能。 参见2

为了做到这一点,我们可以这么做:

#![allow(unused)]
fn main() {
use std::sync::atomic::Ordering;
use std::sync::atomic;
atomic::fence(Ordering::Acquire);
}

最后,我们可以 drop 数据本身。我们使用Box::from_raw来丢弃 Box 中的ArcInner<T>和它的数据。这需要一个*mut T而不是NonNull<T>,所以我们必须使用NonNull::as_ptr进行转换。

unsafe { Box::from_raw(self.ptr.as_ptr()); }

这是安全的,因为我们知道我们拥有的是最后一个指向ArcInner的指针,而且其指针是有效的。

现在,让我们在Drop的实现中把这一切整合起来。

impl<T> Drop for Arc<T> {
    fn drop(&mut self) {
        let inner = unsafe { self.ptr.as_ref() };
        if inner.rc.fetch_sub(1, Ordering::Release) != 1 {
            return;
        }
        // 我们需要防止针对 inner 的使用和删除的重排序,
        // 因此使用 fence 来进行保护是非常有必要的
        atomic::fence(Ordering::Acquire);
        // 安全保证:我们知道这是最后一个对 ArcInner 的引用,并且这个指针是有效的
        unsafe { Box::from_raw(self.ptr.as_ptr()); }
    }
}