【Rust】——引用循环与内存泄漏
Y小夜 2024-08-05 17:05:02 阅读 97
💻博主现有专栏:
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 中被认为是内存安全的。这一点可以通过 <code>Rc<T> 和
RefCell<T>
看出:创建引用循环的可能性是存在的。这会造成内存泄漏,因为每一项的引用计数永远也到不了 0,持有的数据也就永远不会被释放。
🎯制造引用循环
让我们看看引用循环是如何发生的以及如何避免它。
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() {}
现在
Cons
成员的第二个元素是RefCell<Rc<List>>
,这意味着不同于像示例 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<List>
实例来存放初值为5, Nil
的List
值。接着在变量b
中创建了存放包含值 10 和指向列表a
的List
的另一个Rc<List>
实例。
最后,修改
a
使其指向b
而不是Nil
,这就创建了一个循环。为此需要使用tail
方法获取a
中RefCell<Rc<List>>
的引用,并放入变量link
中。接着使用RefCell<Rc<List>>
的borrow_mut
方法将其值从存放Nil
的Rc<List>
修改为b
中的Rc<List>
。
相比真实世界的程序,这个例子中创建引用循环的结果并不可怕。创建了引用循环之后程序立刻就结束了。如果在更为复杂的程序中并在循环里分配了很多内存并占有很长时间,这个程序会使用多于它所需要的内存,并有可能压垮系统并造成没有内存可供使用。
创建引用循环并不容易,但也不是不可能。如果你有包含
Rc<T>
的RefCell<T>
值或类似的嵌套结合了内部可变性和引用计数的类型,请务必小心确保你没有形成一个引用循环;你无法指望 Rust 帮你捕获它们。创建引用循环是一个程序上的逻辑 bug,你应该使用自动化测试、代码评审和其他软件开发最佳实践来使其最小化。
另一个解决方案是重新组织数据结构,使得一部分引用拥有所有权而另一部分没有。换句话说,循环将由一些拥有所有权的关系和一些无所有权的关系组成,只有所有权关系才能影响值是否可以被丢弃。
🎯避免引用循环:将Rc<T>变为Weak<:T>
到目前为止,我们已经展示了调用
Rc::clone
会增加Rc<T>
实例的strong_count
,和只在其strong_count
为 0 时才会被清理的Rc<T>
实例。你也可以通过调用Rc::downgrade
并传递Rc<T>
实例的引用来创建其值的 弱引用(weak reference)。强引用代表如何共享Rc<T>
实例的所有权。弱引用并不属于所有权关系,当Rc<T>
实例被清理时其计数没有影响。它们不会造成引用循环,因为任何涉及弱引用的循环会在其相关的值的强引用计数为 0 时被打断。
调用
Rc::downgrade
时会得到Weak<T>
类型的智能指针。不同于将Rc<T>
实例的strong_count
加 1,调用Rc::downgrade
会将weak_count
加 1。Rc<T>
类型使用weak_count
来记录其存在多少个Weak<T>
引用,类似于strong_count
。其区别在于weak_count
无需计数为 0 就能使Rc<T>
实例被清理。
强引用代表如何共享
Rc<T>
实例的所有权,但弱引用并不属于所有权关系。它们不会造成引用循环,因为任何弱引用的循环会在其相关的强引用计数为 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
值和其子节点引用的Node
:
use std::cell::RefCell;
use std::rc::Rc;
#[derive(Debug)]
struct Node {
value: i32,
children: RefCell<Vec<Rc<Node>>>,
}
我们希望
Node
能够拥有其子节点,同时也希望能将所有权共享给变量,以便可以直接访问树中的每一个Node
,为此Vec<T>
的项的类型被定义为Rc<Node>
。我们还希望能修改其他节点的子节点,所以children
中Vec<Rc<Node>>
被放进了RefCell<T>
。
接下来,使用此结构体定义来创建一个叫做
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<Node>
并储存在branch
中,这意味着leaf
中的Node
现在有两个所有者:leaf
和branch
。可以通过branch.children
从branch
中获得leaf
,不过无法从leaf
到branch
。leaf
没有到branch
的引用且并不知道它们相互关联。我们希望leaf
知道branch
是其父节点。稍后我们会这么做。
🎃增加子到父的作用
为了使子节点知道其父节点,需要在
Node
结构体定义中增加一个parent
字段。问题是parent
的类型应该是什么。我们知道其不能包含Rc<T>
,因为这样leaf.parent
将会指向branch
而branch.children
会包含leaf
的指针,这会形成引用循环,会造成其strong_count
永远也不会为 0。
现在换一种方式思考这个关系,父节点应该拥有其子节点:如果父节点被丢弃了,其子节点也应该被丢弃。然而子节点不应该拥有其父节点:如果丢弃子节点,其父节点应该依然存在。这正是弱引用的例子!
所以
parent
使用Weak<T>
类型而不是Rc<T>
,具体来说是RefCell<Weak<Node>>
。现在Node
结构体定义看起来像这样:
文件名:src/main.rs
use std::cell::RefCell;
use std::rc::{Rc, Weak};
#[derive(Debug)]
struct Node {
value: i32,
parent: RefCell<Weak<Node>>,
children: RefCell<Vec<Rc<Node>>>,
}
这样,一个节点就能够引用其父节点,但不拥有其父节点。在示例 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<Node>
引用,因为branch
并没有父节点。leaf
仍然作为branch
的一个子节点。一旦在branch
中有了Node
实例,就可以修改leaf
使其拥有指向父节点的Weak<Node>
引用。这里使用了leaf
中parent
字段里的RefCell<Weak<Node>>
的borrow_mut
方法,接着使用了Rc::downgrade
函数来从branch
中的Rc<Node>
值创建了一个指向branch
的Weak<Node>
引用。
当再次打印出
leaf
的父节点时,这一次将会得到存放了branch
的Some
值:现在leaf
可以访问其父节点了!当打印出leaf
时,我们也避免了如示例 15-26 中最终会导致栈溢出的循环: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: [] } }] } })
没有无限的输出表明这段代码并没有造成引用循环。这一点也可以从观察
Rc::strong_count
和Rc::weak_count
调用的结果看出。
🎃可视化strong_count和week_count的改变
让我们通过创建了一个新的内部作用域并将
branch
的创建放入其中,来观察Rc<Node>
实例的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<Node>
的强引用计数为 1,弱引用计数为 0。在内部作用域中创建了branch
并与leaf
相关联,此时branch
中Rc<Node>
的强引用计数为 1,弱引用计数为 1(因为leaf.parent
通过Weak<Node>
指向branch
)。这里leaf
的强引用计数为 2,因为现在branch
的branch.children
中储存了leaf
的Rc<Node>
的拷贝,不过弱引用计数仍然为 0。
当内部作用域结束时,
branch
离开作用域,Rc<Node>
的强引用计数减少为 0,所以其Node
被丢弃。来自leaf.parent
的弱引用计数 1 与Node
是否被丢弃无关,所以并没有产生任何内存泄漏!
如果在内部作用域结束后尝试访问
leaf
的父节点,会再次得到None
。在程序的结尾,leaf
中Rc<Node>
的强引用计数为 1,弱引用计数为 0,因为现在leaf
又是Rc<Node>
唯一的引用了。
所有这些管理计数和值的逻辑都内建于
Rc<T>
和Weak<T>
以及它们的Drop
trait 实现中。通过在Node
定义中指定从子节点到父节点的关系为一个Weak<T>
引用,就能够拥有父节点和子节点之间的双向引用而不会造成引用循环和内存泄漏。
上一篇: 【Python】【Pandas】成功解决ValueError: The truth value of a Series is ambiguous. Use a.empty, a.bool(), a.i
下一篇: 详解C/C++输入输出
本文标签
声明
本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。