【Rust】——引用循环与内存泄漏
💻博主现有专栏:
C51单片机(STC89C516),c语言,c++,离散数学,算法设计与分析,数据结构,Python,Java基础,MySQL,linux,基于HTML5的网页设计及应用,Rust(官方文档重点总结),jQuery,前端vue.js,Javaweb开发,Python机器学习等
🥏主页链接:
Y小夜-CSDN博客
目录
🎯制造引用循环
🎃创建树形数据结构:带有子节点的Node
🎃增加子到父的作用
🎃可视化strong_count和week_count的改变
Rust 的内存安全性保证使其难以意外地制造永远也不会被清理的内存(被称为 内存泄漏(memory leak)),但并不是不可能。Rust 并不保证完全防止内存泄漏,这意味着内存泄漏在 Rust 中被认为是内存安全的。这一点可以通过 Rc 和 RefCell 看出:创建引用循环的可能性是存在的。这会造成内存泄漏,因为每一项的引用计数永远也到不了 0,持有的数据也就永远不会被释放。
🎯制造引用循环
让我们看看引用循环是如何发生的以及如何避免它。
use crate::List::{Cons, Nil}; use std::cell::RefCell; use std::rc::Rc; #[derive(Debug)] enum List { Cons(i32, RefCell), Nil, } impl List { fn tail(&self) -> Option { match self { Cons(_, item) => Some(item), Nil => None, } } } fn main() {}
现在 Cons 成员的第二个元素是 RefCell,这意味着不同于像示例 15-24 那样能够修改 i32 的值,我们希望能够修改 Cons 成员所指向的 List。这里还增加了一个 tail 方法来方便我们在有 Cons 成员的时候访问其第二项。
这些代码在 a 中创建了一个列表,一个指向 a 中列表的 b 列表,接着修改 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()); }
这里在变量 a 中创建了一个 Rc 实例来存放初值为 5, Nil 的 List 值。接着在变量 b 中创建了存放包含值 10 和指向列表 a 的 List 的另一个 Rc 实例。
最后,修改 a 使其指向 b 而不是 Nil,这就创建了一个循环。为此需要使用 tail 方法获取 a 中 RefCell 的引用,并放入变量 link 中。接着使用 RefCell 的 borrow_mut 方法将其值从存放 Nil 的 Rc 修改为 b 中的 Rc。
相比真实世界的程序,这个例子中创建引用循环的结果并不可怕。创建了引用循环之后程序立刻就结束了。如果在更为复杂的程序中并在循环里分配了很多内存并占有很长时间,这个程序会使用多于它所需要的内存,并有可能压垮系统并造成没有内存可供使用。
创建引用循环并不容易,但也不是不可能。如果你有包含 Rc 的 RefCell 值或类似的嵌套结合了内部可变性和引用计数的类型,请务必小心确保你没有形成一个引用循环;你无法指望 Rust 帮你捕获它们。创建引用循环是一个程序上的逻辑 bug,你应该使用自动化测试、代码评审和其他软件开发最佳实践来使其最小化。
另一个解决方案是重新组织数据结构,使得一部分引用拥有所有权而另一部分没有。换句话说,循环将由一些拥有所有权的关系和一些无所有权的关系组成,只有所有权关系才能影响值是否可以被丢弃。
🎯避免引用循环:将Rc变为Weak
到目前为止,我们已经展示了调用 Rc::clone 会增加 Rc 实例的 strong_count,和只在其 strong_count 为 0 时才会被清理的 Rc 实例。你也可以通过调用 Rc::downgrade 并传递 Rc 实例的引用来创建其值的 弱引用(weak reference)。强引用代表如何共享 Rc 实例的所有权。弱引用并不属于所有权关系,当 Rc 实例被清理时其计数没有影响。它们不会造成引用循环,因为任何涉及弱引用的循环会在其相关的值的强引用计数为 0 时被打断。
调用 Rc::downgrade 时会得到 Weak 类型的智能指针。不同于将 Rc 实例的 strong_count 加 1,调用 Rc::downgrade 会将 weak_count 加 1。Rc 类型使用 weak_count 来记录其存在多少个 Weak 引用,类似于 strong_count。其区别在于 weak_count 无需计数为 0 就能使 Rc 实例被清理。
强引用代表如何共享 Rc 实例的所有权,但弱引用并不属于所有权关系。它们不会造成引用循环,因为任何弱引用的循环会在其相关的强引用计数为 0 时被打断。
因为 Weak 引用的值可能已经被丢弃了,为了使用 Weak 所指向的值,我们必须确保其值仍然有效。为此可以调用 Weak 实例的 upgrade 方法,这会返回 Option。如果 Rc 值还未被丢弃,则结果是 Some;如果 Rc 已被丢弃,则结果是 None。因为 upgrade 返回一个 Option,Rust 会确保处理 Some 和 None 的情况,所以它不会返回非法指针。
我们会创建一个某项知道其子项和父项的树形结构的例子,而不是只知道其下一项的列表。
🎃创建树形数据结构:带有子节点的Node
在最开始,我们将会构建一个带有子节点的树。让我们创建一个用于存放其拥有所有权的 i32 值和其子节点引用的 Node:
use std::cell::RefCell; use std::rc::Rc; #[derive(Debug)] struct Node { value: i32, children: RefCell, }
我们希望 Node 能够拥有其子节点,同时也希望能将所有权共享给变量,以便可以直接访问树中的每一个 Node,为此 Vec 的项的类型被定义为 Rc。我们还希望能修改其他节点的子节点,所以 children 中 Vec 被放进了 RefCell。
接下来,使用此结构体定义来创建一个叫做 leaf 的带有值 3 且没有子节点的 Node 实例,和另一个带有值 5 并以 leaf 作为子节点的实例 branch
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)]), }); }
这里克隆了 leaf 中的 Rc 并储存在 branch 中,这意味着 leaf 中的 Node 现在有两个所有者:leaf和branch。可以通过 branch.children 从 branch 中获得 leaf,不过无法从 leaf 到 branch。leaf 没有到 branch 的引用且并不知道它们相互关联。我们希望 leaf 知道 branch 是其父节点。稍后我们会这么做。
🎃增加子到父的作用
为了使子节点知道其父节点,需要在 Node 结构体定义中增加一个 parent 字段。问题是 parent 的类型应该是什么。我们知道其不能包含 Rc,因为这样 leaf.parent 将会指向 branch 而 branch.children 会包含 leaf 的指针,这会形成引用循环,会造成其 strong_count 永远也不会为 0。
现在换一种方式思考这个关系,父节点应该拥有其子节点:如果父节点被丢弃了,其子节点也应该被丢弃。然而子节点不应该拥有其父节点:如果丢弃子节点,其父节点应该依然存在。这正是弱引用的例子!
所以 parent 使用 Weak 类型而不是 Rc,具体来说是 RefCell。现在 Node 结构体定义看起来像这样:
文件名:src/main.rs
use std::cell::RefCell; use std::rc::{Rc, Weak}; #[derive(Debug)] struct Node { value: i32, parent: RefCell, children: RefCell, }
这样,一个节点就能够引用其父节点,但不拥有其父节点。在示例 15-28 中,我们更新 main 来使用新定义以便 leaf 节点可以通过 branch 引用其父节点:
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()); }
除了 parent 字段有所不同:leaf 开始时没有父节点,所以我们新建了一个空的 Weak 引用实例。
此时,当尝试使用 upgrade 方法获取 leaf 的父节点引用时,会得到一个 None 值。如第一个 println! 输出所示:
leaf parent = None
当创建 branch 节点时,其也会新建一个 Weak 引用,因为 branch 并没有父节点。leaf 仍然作为 branch 的一个子节点。一旦在 branch 中有了 Node 实例,就可以修改 leaf 使其拥有指向父节点的 Weak 引用。这里使用了 leaf 中 parent 字段里的 RefCell 的 borrow_mut 方法,接着使用了 Rc::downgrade 函数来从 branch 中的 Rc 值创建了一个指向 branch 的 Weak 引用。
当再次打印出 leaf 的父节点时,这一次将会得到存放了 branch 的 Some 值:现在 leaf 可以访问其父节点了!当打印出 leaf 时,我们也避免了如示例 15-26 中最终会导致栈溢出的循环:Weak 引用被打印为 (Weak):
leaf parent = Some(Node { value: 5, parent: RefCell { value: (Weak) }, children: RefCell { value: [Node { value: 3, parent: RefCell { value: (Weak) }, children: RefCell { value: [] } }] } })
没有无限的输出表明这段代码并没有造成引用循环。这一点也可以从观察 Rc::strong_count 和 Rc::weak_count 调用的结果看出。
🎃可视化strong_count和week_count的改变
让我们通过创建了一个新的内部作用域并将 branch 的创建放入其中,来观察 Rc 实例的 strong_count 和 weak_count 值的变化。
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 的强引用计数为 1,弱引用计数为 0。在内部作用域中创建了 branch 并与 leaf 相关联,此时 branch 中 Rc 的强引用计数为 1,弱引用计数为 1(因为 leaf.parent 通过 Weak 指向 branch)。这里 leaf 的强引用计数为 2,因为现在 branch 的 branch.children 中储存了 leaf 的 Rc 的拷贝,不过弱引用计数仍然为 0。
当内部作用域结束时,branch 离开作用域,Rc 的强引用计数减少为 0,所以其 Node 被丢弃。来自 leaf.parent 的弱引用计数 1 与 Node 是否被丢弃无关,所以并没有产生任何内存泄漏!
如果在内部作用域结束后尝试访问 leaf 的父节点,会再次得到 None。在程序的结尾,leaf 中 Rc 的强引用计数为 1,弱引用计数为 0,因为现在 leaf 又是 Rc 唯一的引用了。
所有这些管理计数和值的逻辑都内建于 Rc 和 Weak 以及它们的 Drop trait 实现中。通过在 Node 定义中指定从子节点到父节点的关系为一个Weak引用,就能够拥有父节点和子节点之间的双向引用而不会造成引用循环和内存泄漏。