【Rust自学】15.7. 循环引用导致内存泄漏
说句题外话,这篇文章真心很难,有看不懂可以在评论区问,我会尽快作答的。
喜欢的话别忘了点赞、收藏加关注哦(加关注即可阅读全文),对接下来的教程有兴趣的可以关注专栏。谢谢喵!(=・ω・=)
15.7.1. 内存泄漏
Rust极高的安全性使得内存泄漏很难出现,但并不是完全不可能。
例如使用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() {
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));
}
-
首先创建了一个链表
List
,使用RefCell<T>
包裹Rc<T>
使其内部值可被修改 -
通过
impl
块为List
写了一个叫tail
的方法,用于获取List
下Cons
变体附带的第二个元素,如果有就返回其值,用Some
封装,是Nil
就返回None
。 -
然后在
main
函数创建了a
、b
两个List
的实例,b
内部共享了a
的值。这种链表的代码看着就犯恶心,所以我把其结构图放在
这里: -
main
函数里还通过Rc::strong_count
获取了a
和b
的强引用数量,使用自定义的tail
方法获了Cons
附带的第二个元素,用println!
打印出来。 -
下面使用
if let
语句把a
的Cons
的第二个值绑在link
上,通过borrow_mut
方法获得其可变引用&Cons
,使用解引用符号*
把它转为Cons
,最后把b
的值通过Rc::clone
共享赋给了link
,也就改变了a
内部的结构,变为了:
PS:我觉得自己画的太烂了,所以这里就换成The Rust Programming Language里的图片了
输出:
a initial rc count = 1
a next item = Some(RefCell { value: Nil })
a rc count after b creation = 2
b initial rc count = 1
b next item = Some(RefCell { value: Cons(5, RefCell { value: Nil }) })
b rc count after changing a = 2
a rc count after changing a = 2
第1行到第5行:刚开始创建a
时,引用数量就为1,当b
被声明时,a
被共享了,所以此时a
的引用计数为2,b
为1。
第六行到第7行:if let
语句把a
的内部结构改变了,使a
的第二个元素指向b
,b
的引用数量加1变为2。此时a
指向了b
,b
又指向了a
,就会造成循环引用。
当a
和b
都走出了作用域,Rust删除了变量b
,这将b
的引用计数从 2 减少到 1。此时Rc<List>
在堆上的内存不会被删除,因为它的引用计数是1,而不是0。然后 Rust 删除a
,这会将a
的Rc<List>
实例的引用计数从 2 减少到 1,如图所示。这个实例的内存也不能被删除,因为另一个实例的内存 Rc<List>
实例仍然引用它。分配给列表的内存将永远保持未回收状态。
接下来我们看看循环引用的内容是什么,使用这条代码:
println!("a next item = {:?}", a.tail());
Rust 将尝试打印此循环,其中a
指向b
指向a
等等,直到溢出堆栈。最终的结果会是栈溢出错误。
15.7.2. 防止内存泄漏的方法
那有什么方法来防止内存泄漏吗?这只能依靠开发者,不能依靠Rust。
不然就只能重新组织数据结构,把引用拆分成持有和不持有所有权的两种情况,一些引用用来表达所有权,一些引用不表达所有权。循环引用的一部分具有所有权关系,另一部分不涉及所有权关系。这样写只有所有权的指向关系才会影响到值的清理。
15.7.3. 把Rc<T>
换成Weak<T>
以防止循环引用
我们知道Rc::clone
会生成数据的强引用,使Rc<T>
内部的引用计数加1,而Rc<T>
只有在strong_count
为0时才会被清理。
然而,Rc<T>
实例通过调用Rc::downgrade
方法创建值的弱引用(Weak Reference)。这个方法的返回类型是weak<T>
(也是智能指针),每次调用Rc::downgrade
会为weak_count
加1而不是strong_count
,所以弱引用并不影响Rc<T>
的清理。
15.7.4. Strong vs. Weak
强引用(Strong Reference)是关于如何分析Rc<T>
实例的所有权。弱引用(Weak Reference)并不表达上述意思,使用它不会创建循环引用:当强引用数量为0时,弱引用就会自动断开。
使用弱引用之前需要保证它指向的值仍然存在。在Weak<T>
实例上调用upgrade
方法,返回Option<Rc<T>>
,通过Option
枚举来完成值是否存在的验证。
看个例子:
use std::cell::RefCell;
use std::rc::Rc;
#[derive(Debug)]
struct Node {
value: i32,
children: RefCell<Vec<Rc<Node>>>,
}
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)]),
});
}
Node
结构体代表一个节点,有两个字段:
value
字段存储当前值,类型是i32
。children
字段存储子节点,类型是RefCell<Vec<Rc<Node>>>
,这里使用Rc<T>
是为了让所有子节点共享所有权。具体来说,我们希望一个Node
拥有它的子节点,并且我们希望与储存这个节点的变量共享该所有权,以便我们可以直接访问树中的每个Node
。为此,我们将Vec<T>
项定义为Rc<Node>
类型的值。
这个例子的需求是每个节点都能指向自己的父节点和子节点。
再看一下main
函数:
- 创建了
leaf
,是Node
实例,value
为3,children
的值是被RefCell
包裹的空Vector
。 - 创建了
branch
,是Node
实例,value
为5,children
的值指向了leaf
。
这意味着leaf
它里面的Node
节点有两个所有者。目前可以通过branch
的children
字段访问leaf
;而反过来如果想通过leaf
来访问branch
暂时还不行,所以这里还需要修改。
想要实现需求就得用双向引用,但是双向的引用会创建循环引用,所以这时候就得使用Weak<T>
,避免产生循环:
struct Node {
value: i32,
parent: RefCell<Weak<Node>>,
children: RefCell<Vec<Rc<Node>>>,
}
添加了parent
字段表示父节点,使用弱引用Weak<T>
。这里不用Vec<>
是因为这是个树结构,父节点只可能有一个。
这么写得把Weak<T>
引入作用域,还得重构下文,修改完后的整体代码如下:
use std::cell::RefCell;
use std::rc::{Rc, Weak};
#[derive(Debug)]
struct Node {
value: i32,
parent: RefCell<Weak<Node>>,
children: RefCell<Vec<Rc<Node>>>,
}
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());
}
在leaf
被创建后先打印了其parent
字段的内容(这时parent
字段还没有内容);在branch
被创建后打印了leaf
的parent
字段内容(这时其内容就是branch
)。
*leaf.parent.borrow_mut() = Rc::downgrade(&branch);
这句话把branch
的内容从Rc<Node>
变为Weak<Node>
,指向了leaf
的parent
字段:
leaf.parent
是表示leaf
父节点的字段,其类型是RefCell<Weak<Node>>
,所以可以使用borrow_mut
来获得其可变引用&mut RefMut<Weak<Node>>
- 使用解引用符号
*
把可变引用&mut RefMut<Weak<Node>>
变为RefMut<Weak<Node>>
- 通过
downgrade
方法把branch
的Rc<Node>
变为Weak<Node>
并赋给parent
输出:
leaf parent = None
leaf parent = Some(Node { value: 5, parent: RefCell { value: (Weak) },
children: RefCell { value: [Node { value: 3, parent: RefCell { value: (Weak) },
children: RefCell { value: [] } }] } })
- 第一次打印时其
parent
字段还没有被赋值,所以其值是Option
下的None
变体。 - 第二次打印时其父节点已被指定为
branch
,不是无限输出表明此代码没有创建循环引用。
最后我们通过修改main
函数——添加打印语句和修改作用域来看看强引用和弱引用的数量:
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
之后打印里面有多少强引用和弱引用 -
这部分完了之后加了
{}
,创建了新的作用域:- 把
branch
的声明和指定leaf
父节点的操作放到里面 - 打印
branch
和leaf
在此时强引用、弱引用的数量
- 把
-
走出作用域后:
- 打印
leaf
的parent
- 打印
leaf
的强引用、弱引用
- 打印
输出:
leaf strong = 1, weak = 0
branch strong = 1, weak = 1
leaf strong = 2, weak = 0
leaf parent = None
leaf strong = 1, weak = 0
- 第1行:创建了
leaf
,只有一个强引用 - 第2行:创建了
branch
,由于branch
使用强引用对leaf
进行了关联,其parent
字段使用了Weak::new()
创建,所以branch
有1个强引用,一个弱引用 - 第3行:
branch
使用了leaf
的强引用,其本身在声明时又是一个强引用,所以此时leaf
就有两个强引用 - 第4行:由于
branch
已经走出其作用域,所以leaf
的parent
字段此时就为None
- 第5行:
branch
已经走出其作用域导致它对leaf
的强引用失效,leaf
的强引用减1变为1