• Rust源码分析——Rc 和 Weak 源码详解


    Rc 和 Weak 源码详解

    一个值需要被多个所有者拥有

    1. rust中所有权机制在图这种数据结构中,一个节点可能被多个其它节点所指向。那么如何表示图这种数据结构?
    2. 在多线程中,多个线程可能会持有同一个数据?如何解决这个问题。

    Rc

    rust 通过使用引用计数智能指针 Rc 和 Arc 来解决上面的问题。当我们对一个被 Rc 所标识的数据进行 clone() 的时候,并不会复制其内部数据,只是增加引用计数,而当一个 Rc 被 drop 的时候,只会减少其引用计数,直到引用计数为0,此时才会真正清除对应的内存。

    但是使用引用计数方案有一个问题,那就是如何解决循环引用问题?如果不了解引用计数方式管理内存的,可以看这篇文章。rust 为了解决这个问题,提供了弱引用(Weak)。它不拥有数据的所有权,只产生弱引用计数。

    我们来看一下 Rc 这个结构

    #[cfg_attr(not(test), rustc_diagnostic_item = "Rc")]
    #[stable(feature = "rust1", since = "1.0.0")]
    #[rustc_insignificant_dtor]
    pub struct Rc<T: ?Sized> {
        ptr: NonNull<RcBox<T>>,
        phantom: PhantomData<RcBox<T>>,
    }
    
    #[stable(feature = "rust1", since = "1.0.0")]
    impl<T: ?Sized> !Send for Rc<T> {}
    
    // Note that this negative impl isn't strictly necessary for correctness,
    // as `Rc` transitively contains a `Cell`, which is itself `!Sync`.
    // However, given how important `Rc`'s `!Sync`-ness is,
    // having an explicit negative impl is nice for documentation purposes
    // and results in nicer error messages.
    #[stable(feature = "rust1", since = "1.0.0")]
    impl<T: ?Sized> !Sync for Rc<T> {}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    首先,Rc 是一个结构体,可以看到它不满足 Send 和 Sync 这两个 trait,这意味着 Rc 是不能跨线程的,它只适用于单线程下的引用计数。这是 rust 专门为单线程场景设计的高性能引用计数器;而多线程下需要 Arc (atomic reference counting)来实现多线程的引用计数。

    另外一点就是 Rc 接受的泛型参数可以是大小未知(unsized)类型。Rc 结构体中有两个字段 ptr 和 phantom 。ptr 的类型是NonNull>

    pub struct NonNull<T: ?Sized> {
        pointer: *const T,
    }
    
    • 1
    • 2
    • 3

    也就是说 ptr 实际上是一个指向 RcBox 的非空指针。OK,我们接着来看一下 RcBox 类型

    struct RcBox<T: ?Sized> {
        strong: Cell<usize>,
        weak: Cell<usize>,
        value: T,
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    下面,让我来详细解释这个结构体的各个字段:

    1. strong: Cell:这个字段是一个 Cell 类型的包装,用于存储强引用计数(strong reference count)。Cell 是 rust标准库提供的一种允许在不可变情况下修改其内部值的类型。强引用计数用于跟踪有多少个 Rc 实例仍然拥有对数据的引用。每当创建一个新的 Rc 引用时,强引用计数会递增;当 Rc 引用离开作用域或被丢弃时,强引用计数递减。

    2. weak: Cell:这个字段是一个 Cell 类型的包装,用于存储弱引用计数(weak reference count)。弱引用计数用于跟踪有多少个 Weak 引用(Rc 的弱引用)仍然存在,但它不会阻止数据的销毁。与强引用不同,当只有弱引用剩余时,数据可以被销毁。每当创建一个新的 Weak 引用时,弱引用计数会递增;当Weak 引用离开作用域或被丢弃时,弱引用计数递减。

    3. value: T:这是 Rc 包装的实际值的字段。Rc 用于共享这个值,因此它包含在 RcBox 中。

    既然强引用,弱引用以及值都包含在 RcBox 中了,那么 phantom: PhantomData> 的作用是什么?

    PhantomData 是一个泛型类型,通常用于标记类型参数在运行时不实际占用内存。在这里,它用于确保 RcBox 存在,尽管它在运行时不占用内存。这是为了帮助Rust编译器进行正确的类型检查和生命周期分析。

    pub struct PhantomData<T: ?Sized>;
    
    • 1

    正如我们所见,PhantomData 是一个单元结构体,它的大小是零字节,不占用内存空间。

    我们进一步来看一下 Rc 的构造方法,看看它到底是如何做到让一个值可以有多个所有者?按照之前的一个值只有一个所有者的模型,当所有者生命周期结束的时候,值就会被回收;而 Rc 是在强引用计数到 0 的时候,释放内存。

    pub fn new(value: T) -> Rc<T> {
        // There is an implicit weak pointer owned by all the strong
        // pointers, which ensures that the weak destructor never frees
        // the allocation while the strong destructor is running, even
        // if the weak pointer is stored inside the strong one.
        unsafe {
            Self::from_inner(
                Box::leak(Box::new(RcBox { strong: Cell::new(1), weak: Cell::new(1), value }))
                    .into(),
            )
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    首先,我们注意到 new 的实现代码是 unsafe 的,这是因为 Box::leak 方法将 Box 中的数据泄漏(leak)出来,而这个操作将绕过 Rust 的所有权和生命周期检查,这样 RcBox 结构体数据将被泄漏到堆上,使其在函数结束后继续存在,而不是按正常方式被释放,通过这种手段,让 RcBox 拥有了足够长的生命周期,以便在多个 Rc 实例之间正确地共享数据。

    这段代码的注释中还告诉了我们:所有强引用指针(Rc 实例)之间都存在一个隐式的弱引用指针。这个隐式的弱引用用于确保在强引用的析构函数运行期间,弱引用不会释放数据,即使在强引用指针中存储了一个弱引用。后面当我们介绍 Weak 析构函数的时候,会看到它需要先读取 RcBox 中的数据。这样就防止弱引用析构执行的时候会访问到悬垂指针。

    接着,我们来看一下析构函数的代码。

    fn drop(&mut self) {
        unsafe {
            self.inner().dec_strong();      // 强引用计数减 1
            if self.inner().strong() == 0 {
                // destroy the contained object
                ptr::drop_in_place(Self::get_mut_unchecked(self));
    
                // remove the implicit "strong weak" pointer now that we've
                // destroyed the contents.
                self.inner().dec_weak();    // 弱引用计数减 1
    
                if self.inner().weak() == 0 {
                    Global.deallocate(self.ptr.cast(), Layout::for_value(self.ptr.as_ref()));
                }
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    1. 如果强引用计数为零,表示没有任何强引用指向数据了,这意味着数据可以安全地被销毁。
    2. 如果弱引用计数降至零,表示没有任何弱引用指向数据,将弱引用相关的资源清理掉。

    既然 RcBox 中也存储了弱引用计数,那么 Rc 肯定提供了从一个 Rc 获取到 弱引用的方法。实际上就是 downgrade 方法

    pub fn downgrade(this: &Self) -> Weak<T> {
        this.inner().inc_weak();
        // Make sure we do not create a dangling Weak
        debug_assert!(!is_dangling(this.ptr.as_ptr()));
        Weak { ptr: this.ptr }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    这个函数非常简单,让弱引用计数加1,然后保证不是悬垂指针之后,用这个指针作为参数构造了一个 Weak 返回。这样就实现了从 Rc 中获取 Weak。

    Weak

    我们顺便来看一下弱引用,Weak 用于创建弱引用,通常与 Rc 智能指针一起使用。

    pub struct Weak<T: ?Sized> {
        // This is a `NonNull` to allow optimizing the size of this type in enums,
        // but it is not necessarily a valid pointer.
        // `Weak::new` sets this to `usize::MAX` so that it doesn’t need
        // to allocate space on the heap. That's not a value a real pointer
        // will ever have because RcBox has alignment at least 2.
        // This is only possible when `T: Sized`; unsized `T` never dangle.
        ptr: NonNull<RcBox<T>>,
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    Weak 也存储了一个指向 RcBox 的指针。看起来这是比 Rc 少了一个标记字段,实际上它们的构造函数完全不同。

    pub const fn new() -> Weak<T> {
        Weak { ptr: unsafe { NonNull::new_unchecked(ptr::invalid_mut::<RcBox<T>>(usize::MAX)) } }
    }
    
    • 1
    • 2
    • 3

    ptr::invalid_mut 函数来创建一个无效的指针,其值被设置为 usize::MAX。这个无效指针用于表示一个 Weak 弱引用指针,它不引用任何真实的数据,但是用于表示一个空的 Weak 实例,然后将其包装在 NonNull 中,并返回作为 Weak 实例的一部分。这个无效的 Weak 实例通常用于初始化,之后可以使用 upgrade 方法来尝试获取一个真实的强引用。

    实际上,在 Weak 结构体的注释中已经解释了 new 方法为什么会是这样。设置为 usize::MAX 的目的是为了避免在创建 Weak 时需要分配堆内存。由于 Weak 通常用于检查数据的存在性而不需要实际引用数据。

    我们再来看一下析构函数,

    fn drop(&mut self) {
        let inner = if let Some(inner) = self.inner() { inner } else { return };
    
        inner.dec_weak();   // 弱引用计数减1
        // the weak count starts at 1, and will only go to zero if all
        // the strong pointers have disappeared.
        if inner.weak() == 0 {
            unsafe {
                Global.deallocate(self.ptr.cast(), Layout::for_value_raw(self.ptr.as_ptr()));
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    let inner = if let Some(inner) = self.inner() { inner } else { return };:这一行代码的目的是获取 Weak 引用内部的 RcBox 数据结构,以便后续操作。self.inner() 方法用于获取内部数据,如果存在则返回 Some(inner),否则返回 None。如果不存在内部数据,说明这个 Weak 已经被销毁,所以函数提前返回(return)。

    如果弱引用计数降至零,说明没有任何弱引用指向数据,这意味着数据可以被释放。此时使用 Global.deallocate 来释放和 Weak 相关的内存。

    前面说过可以通过 Rc 获取到一个弱引用,那么同样,当我们需要通过 Weak 来获取数据的时候,就会产生一个 Rc。这个时候就需要使用 Weak 提供的 upgrade 方法。

    pub fn upgrade(&self) -> Option<Rc<T>> {
        let inner = self.inner()?;
    
        if inner.strong() == 0 {
            None
        } else {
            unsafe {
                inner.inc_strong();
                Some(Rc::from_inner(self.ptr))
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    首先,尝试获取 RcBox 中的数据,如果是 None,则直接返回,否则获取到 RcBox 中的数据,进行强引用计数判断,如果强引用计数为 0,那么意味着数据被释放,返回 None,否则将强引用计数加 1,然后返回一个 Rc 实例。

    参考资料

    Rust 官方文档: https://doc.rust-lang.org/std/rc/struct.Rc.html

  • 相关阅读:
    React + Antd 自定义Select选择框 全选、清空功能
    没有学过C语言可以学Java吗?
    【无标题】
    数仓工具—Hive语法之Merge 语句
    postfix 禁用附件功能
    真香!超全,Python 中常见的配置文件写法
    ubuntu配置jdk
    java调用c函数
    HtmlJavaScript的 getElementBYId和querySelector速度性能对比测试 2210011540
    外汇天眼:Equals集团董事会成功争取PUSU截止日期延期
  • 原文地址:https://blog.csdn.net/zy010101/article/details/132797374