Rust 学习笔记:循环引用可能导致内存泄漏

Rust 学习笔记:循环引用可能导致内存泄漏

使用 Rc<T> 和 RefCell<T> 可能造成项在一个循环中相互引用。因为循环中每个项的引用计数永远不会达到 0,并且这些值永远不会被丢弃,导致内存泄漏。

创建一个循环引用

让我们从 List 枚举和 tail 方法的定义开始,看看引用循环是如何发生的,以及如何防止它。

use crate::List::{Cons, Nil};
use std::cell::RefCell;
use std::rc::Rc;

#[derive(Debug)]
enum List {
    Cons(i32, RefCell<Rc<List>>),
    Nil,
}

impl List {
    fn tail(&self) -> Option<&RefCell<Rc<List>>> {
        match self {
            Cons(_, item) => Some(item),
            Nil => None,
        }
    }
}

fn main() {}

重新定义了 List,Cons 变量中的第二个元素现在是 RefCell<Rc<List>>,这意味着我们不能修改 i32 值,而是要修改 Cons 变量所指向的 List 值。我们还添加了一个 tail 方法,以便在有 Cons 变量时方便地访问第二个项。

接着实现 main 函数,在 a 中创建了一个链表,初始列表为 5、Nil。在 b 中创建了一个指向 a 中的链表的链表。然后修改 a 中的链表,使其指向 b,从而创建了一个引用循环。在此过程中有 println! 语句显示该过程中各个点的引用计数。

fn main() {
    let a = Rc::new(Cons(5, RefCell::new(Rc::new(Nil))));

    println!("a initial rc count = {}", Rc::strong_count(&a));
    println!("a next item = {:?}", a.tail());

    let b = Rc::new(Cons(10, RefCell::new(Rc::clone(&a))));

    println!("a rc count after b creation = {}", Rc::strong_count(&a));
    println!("b initial rc count = {}", Rc::strong_count(&b));
    println!("b next item = {:?}", b.tail());

    if let Some(link) = a.tail() {
        *link.borrow_mut() = Rc::clone(&b);
    }

    println!("b rc count after changing a = {}", Rc::strong_count(&b));
    println!("a rc count after changing a = {}", Rc::strong_count(&a));

    // Uncomment the next line to see that we have a cycle;
    // it will overflow the stack.
    // println!("a next item = {:?}", a.tail());
}

我们通过使用 tail 方法来获取 a 中的 RefCell<Rc<List>> 的引用,我们将其放入变量 link 中。然后,我们在 RefCell<Rc<List>> 上使用 borrow_mut 方法将内部的值从 Rc<List>(保存 Nil)更改为 b 中的 Rc<List>。

程序输出:

在这里插入图片描述

程序创建了一个循环引用:

在这里插入图片描述

在我们将 a 中的 List 改为指向 b 后,a 和 b 中的 Rc<List> 实例的引用计数都是 2。在 main 函数的末尾,Rust 删除变量 b,这将使 b Rc<List> 实例的引用计数从 2 减少到 1,同理也将 a Rc<List> 实例的引用计数减少到 1。这两个 Rc<List> 在堆上的内存此时不会被丢弃,分配给列表的内存将永远保持未回收状态。

如果取消注释最后一个 println! 然后运行程序,Rust 将尝试打印这个循环,a 指向 b, b 指向 a,以此类推,直到它溢出堆栈。

在这里插入图片描述

循环引用是程序中的逻辑错误,不能被 Rust 发现并纠正。应该使用自动化测试、代码审查和其他软件开发实践来最小化该错误。

避免引用循环的一个解决方案是重新组织数据结构,以便一些引用表示所有权,而另一些引用不表示所有权。

使用 Weak<T> 防止引用循环

到目前为止,我们已经演示了调用 Rc::clone 创建强引用,这会增加 Rc<T> 实例的 strong_count,而 Rc<T> 实例只有在其 strong_coun t为 0 时才会被清理。

可以通过调用 Rc::downgrade 并将引用传递给 Rc<T>,从而在 Rc<T> 实例中创建对该值的弱引用。弱引用不表达所有权关系,当 Rc<T> 实例被清理时,它们的计数不受影响。

弱引用不会导致引用循环,因为一旦所涉及的值的强引用计数为 0,任何涉及弱引用的循环都会被打破。

当调用 Rc::downgrade 方法时,你将获得一个 Weak<T> 类型的智能指针。g该方法不是将 Rc<T> 实例中的 strong_count 增加 1,而是将 weak_count 增加1 。Rc<T> 类型使用 weak_count 来跟踪存在多少个 Weak<T> 引用,类似于 strong_count。不同之处在于,要清除 Rc<T> 实例,weak_count 不必为 0。

因为 Weak<T> 引用的值可能已经被删除了,所以要对 Weak<T> 指向的值做任何操作,必须确保该值仍然存在。通过在一个 Weak<T> 实例上调用 upgrade 方法来实现这一点,该方法将返回一个 Option<Rc<T>>。如果 Rc<T> 值尚未被删除,将得到结果 Some,如果 Rc<T> 值已被删除,将得到结果 None。因为 upgrade 返回 Option<Rc<T>>, Rust 将确保 Some 和 None 情况得到处理,并且不会有无效指针。

我们以树形数据结构为例进行讲解。

创建树形数据结构:带子节点的节点

首先,我们将构建一个树,其中的节点知道它们的子节点。

创建一个名为 Node 的结构体,它保存自己的 i32 值以及对其子节点值的引用:

use std::cell::RefCell;
use std::rc::Rc;

#[derive(Debug)]
struct Node {
    value: i32,
    children: RefCell<Vec<Rc<Node>>>,
}

我们希望一个节点拥有它的子节点,并且我们希望与变量共享这个所有权,这样我们就可以直接访问树中的每个节点。为此,我们将 Vec<T> 项定义为 Rc<Node> 类型的值。我们还想修改子节点,所以我们在 Vec<Rc<Node>> 外套上了一层 RefCell<T>。

接下来,我们创建一个名为 leaf 的 Node 实例,它的值为 3,没有子节点。再创建一个名为 branch 的实例,它的值为 5,leaf 是它的子节点之一。

fn main() {
    let leaf = Rc::new(Node {
        value: 3,
        children: RefCell::new(vec![]),
    });

    let branch = Rc::new(Node {
        value: 5,
        children: RefCell::new(vec![Rc::clone(&leaf)]),
    });
}

我们使用 Rc::clone 将 leaf 存储在 branch.children 中,这意味着 leaf 中的 Node 现在有两个所有者:leaf 和 branch。

我们可以通过 branch.children 从 branch 到 leaf,但是没有办法从 leaf 到 branch。

我们想让 leaf 知道 branch 是它的父结点,在下一小节实现。

添加一个从子节点向父节点的引用

为了让子节点知道它的父节点,我们需要在 Node 结构体的定义中添加一个 parent 字段。

显然 Rc<T> 是不行的,leaf.parent 指向 branch,branch.children 指向 leaf,这将使得 leaf 和 branch 的 strong_count 值永远不会为 0,导致循环引用。

从另一个角度考虑关系,父节点应该拥有它的子节点:如果父节点被删除,它的子节点也应该被删除。但是,子节点不应该拥有它的父节点:如果我们删除一个子节点,父节点应该仍然存在。!

这是一个弱引用的例子!因此,我们将使 parent 字段的类型定义为 RefCell<Weak<Node>>,现在我们的 Node 结构体定义为:

use std::cell::RefCell;
use std::rc::{Rc, Weak};

#[derive(Debug)]
struct Node {
    value: i32,
    parent: RefCell<Weak<Node>>,
    children: RefCell<Vec<Rc<Node>>>,
}

一个节点可以引用它的父节点,但不拥有它的父节点。更新 main 函数,这样叶子节点就有了一种方法来引用它的父节点分支。

fn main() {
    let leaf = Rc::new(Node {
        value: 3,
        parent: RefCell::new(Weak::new()),
        children: RefCell::new(vec![]),
    });

    println!("leaf parent = {:?}", leaf.parent.borrow().upgrade());

    let branch = Rc::new(Node {
        value: 5,
        parent: RefCell::new(Weak::new()),
        children: RefCell::new(vec![Rc::clone(&leaf)]),
    });

    *leaf.parent.borrow_mut() = Rc::downgrade(&branch);

    println!("leaf parent = {:?}", leaf.parent.borrow().upgrade());
}

起初,当我们试图通过使用 upgrade 方法获取对 leaf 父节点的引用时,我们得到一个 None 值:

leaf parent = None

之后,程序修改 leaf,为它提供一个对其父节点的 Weak<Nod\e> 引用。我们在 leaf 的 parent 字段中的 RefCell<Weak<Node>> 上使用 borrow_mut 方法,得到 Weak<Node> 的可变引用,然后我们使用 Rc::downgrade 从 branch 中的 Rc<Node> 创建一个到 branch 的 Weak<Node> 引用。

当我们再次打印 leaf 的父节点的引用时,这次我们将得到一个包含 branch 的 Some 变量。Weak<Node> 引用被打印为 (Weak)。

leaf parent = Some(Node { value: 5, parent: RefCell { value: (Weak) }, children: RefCell { value: [Node { value: 3, parent: RefCell { value: (Weak) }, children: RefCell { value: [] } }] } })

观察 strong_count 和 weak_count 的变化

让我们看看 Rc<Node> 实例的 strong_count 和 weak_count 值是如何通过创建一个新的内部作用域并将 branch 的创建移动到该作用域中来改变的。

修改之前的代码:

fn main() {
    let leaf = Rc::new(Node {
        value: 3,
        parent: RefCell::new(Weak::new()),
        children: RefCell::new(vec![]),
    });

    println!(
        "leaf strong = {}, weak = {}",
        Rc::strong_count(&leaf),
        Rc::weak_count(&leaf),
    );

    {
        let branch = Rc::new(Node {
            value: 5,
            parent: RefCell::new(Weak::new()),
            children: RefCell::new(vec![Rc::clone(&leaf)]),
        });

        *leaf.parent.borrow_mut() = Rc::downgrade(&branch);

        println!(
            "branch strong = {}, weak = {}",
            Rc::strong_count(&branch),
            Rc::weak_count(&branch),
        );

        println!(
            "leaf strong = {}, weak = {}",
            Rc::strong_count(&leaf),
            Rc::weak_count(&leaf),
        );
    }

    println!("leaf parent = {:?}", leaf.parent.borrow().upgrade());
    println!(
        "leaf strong = {}, weak = {}",
        Rc::strong_count(&leaf),
        Rc::weak_count(&leaf),
    );
}

程序输出:

在这里插入图片描述

在创建 leaf 之后,它的 Rc<Node> 的强引用计数为 1,弱引用计数为 0。

在内部作用域中,我们创建 branch 并将其与 leaf 关联,此时当我们打印计数时,branch 中的 Rc<Node> 将具有强引用计数 1 和弱引用计数 1。

当我们在 leaf 中打印计数时,我们会看到它将有一个强引用计数 2,因为现在有一个 leaf 的 Rc<Node> 的克隆存储在 branch.children 中,但弱引用计数仍为 0。

当内部作用域结束时,branch 退出作用域,它的 Rc<Node> 的强引用计数减少到 0,因此它的 Node 被销毁,即使其弱引用计数为 1。

如果我们试图在作用域结束后访问 leaf 的父节点,我们将得到 None。在程序结束时,leaf 中的 Rc<Node> 的强计引用数为 1,弱引用计数为 0,因为变量 leaf 现在是对 Rc<Node> 的唯一引用。

所有管理引用计数和值删除的逻辑都内置在 Rc<T> 和 Weak<T> 以及它们的 Drop trait 实现中。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

UestcXiye

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值