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 实现中。