引用循环可能会泄露内存
Reference Cycles Can Leak Memory
Rust 的内存安全保证,使得意外创建出从未清理过的内存(称为 内存泄漏,memory leak)很难,但并非不可能。完全防止内存泄漏不是 Rust 的保证之一,这意味着内存泄漏在 Rust 中是内存安全的。通过使用 Rc<T>
和 RefCell<T>
,我们可以看到 Rust 允许内存泄漏:创建出其中项目在循环中相互指向的引用是可能的。这会造成内存泄漏,因为循环中各个项目的引用计数永远不会达到 0,而值永远不会被弃用。
创建引用循环
Creaing a Reference Cycle
咱们以清单 15-25 中的 List
枚举和 tail
方法开始,来看看循环引用是如何发生的,以及怎样防止他:
文件名:src/main.rs
#![allow(unused)] fn main() { {{#include ../projects/ref_cycle_demo/src/main.rs::18}} }
清单 15-25:包含 RefCell<T>
的构造列表定义,因此我们可以修改 Cons
变种指向的内容
我们正在使用清单 15-5
中 List
定义的另一种变体。 Cons
变种中的第二个元素现在是 RefCell<Rc<List>>
,这意味着我们不像在示例 15-24
中那样能够修改 i32
值,我们打算修改 Cons
变种指向的 List
值。我们还添加了一个 tail
方法,以便在我们有 Cons
变种时方便地访问第二个项目。
在下面清单 15-26 中,咱们添加了用到清单 15-25 中那些定义的 main
函数。此代码创建了变量 a
中的一个清单,以及变量 b
中指向 a
中清单的一个清单。随后他将 a
中的清单指向了 b
,这就创建了一个循环引用。其间有着一些 println!
语句,来显示此过程中不同点位的那些引用计数。
文件名:src/main.rs
#![allow(unused)] fn main() { {{#include ../projects/ref_cycle_demo/src/main.rs:20:}} }
清单 15-26:创建出相互指向的两个 List
的循环引用
我们创建一个 Rc<List>
实例,在变量 a
中持有一个 List
值,初始列表为 5, Nil
。然后我们创建一个 Rc<List>
实例,在变量 b
中保存另一个 List
值,其中包含值 10
,并指向 a
中的列表。
我们修改 a
使其指向 b
而不是 Nil,从而创建一个循环。为此,我们使用 tail
方法获取对 a
中 RefCell<Rc<List>>
的引用,我们将其放入变量 link
中。然后我们使用 RefCell<Rc<List>>
上的 borrow_mut
方法,将里面的值从一个持有 Nil
值的 Rc<List>
更改为 b
中的 Rc<List>
。
咱们暂时保持最后的 println!
注释掉,而运行此代码时,咱们将得到下面的输出:
$ cargo run
Compiling ref_cycle_demo v0.1.0 (/home/lennyp/rust-lang/ref_cycle_demo)
Finished dev [unoptimized + debuginfo] target(s) in 1.20s
Running `target/debug/ref_cycle_demo`
a 的初始 rc 计数 = 1
a 的下一条目 = Some(RefCell { value: Nil })
b 的创建后 a 的 rc 计数 = 2
b 的初始 rc 计数 = 1
b 的下一条目 = Some(RefCell { value: Cons(5, RefCell { value: Nil }) })
在修改 a 之后 b 的 rc 计数 = 2
在修改 a 之后 a 的 rc 计数 = 2
在咱们将 a
中的列表改为指向 b
后,a
和 b
中的 Rc<List>
实例的引用计数均为 2
。在 main
的最后,Rust 弃用了变量 b
,这使得 b
中的 Rc<List>
实例的引用计数从 2
减少到 1
。Rc<List>
在内存堆中的内存此时不会被弃用,因为其引用计数为 1
而不是 0
。然后 Rust 弃用 a
,将 a
中的 Rc<List>
实例的引用计数从 2
减少到 1
。由于另一 Rc<List>
实例仍指向他,因此该实例的内存也不能被弃用。分配给列表内存将永远保持未被收集的状态。为直观地表示这个引用循环,咱们创建了下图 15-4 中的图表。
图 15-04:列表 a
和 b
相互指向的引用循环
如果咱们取消对最后一个 println!
的注释并运行程序,Rust 会尝试打印这个循环,a
指向 b
指向 a
,如此反复,直到程序溢出栈,overflows the stack。
与现实世界的程序相比,在这个例子中创建一个引用循环的后果并不可怕:在我们创建引用循环之后,程序就结束了。然而,如果某个更复杂的程序在一个循环中分配了大量的内存,并长期占用这些内存,那么这个程序将使用比其需要的更多的内存,并可能使系统不堪重负,导致其可用内存耗尽。
创建引用循环不容易做到,但也不是不可能。如果咱们有着其中包含 Rc<T>
值的 RefCell<T>
值,或类似的带有内部可变性与引用计数类型的嵌套组合,咱们必须确保不创建循环;咱们不能依靠 Rust 来捕获他们。创建引用循环将是咱们程序中的逻辑错误,咱们应该使用自动测试、代码审查和其他软件开发实践来减少这种错误。
另一避免引用循环的办法是重新组织咱们的数据结构,使一些引用表达所有权,而一些引用不表达。由此,咱们可以有由一些所有权关系和一些非所有权关系组成的循环,而只有所有权关系会影响一个值是否可以被丢弃。在清单 15-25 中,我们总是希望 Cons
变体拥有他们的列表,所以重新组织数据结构是不可能的。咱们来看一个使用由父节点和子节点组成的图的示例,看看什么时候非所有权关系是防止引用循环的合适方式。
防止引用循环:将 Rc<T>
变为 Weak<T>
Preventing Referencing Cycles: Turning an Rc<T>
into Weak<T>
到目前为止,我们已经证明了调用 Rc::clone
会增加 Rc<T>
实例的 strong_count
,而 Rc<T>
实例只有在其 strong_count
为 0
时才会被清理掉。咱们还可以通过调用 Rc::downgrade
并传递对 Rc<T>
的引用,来创建对 Rc<T>
实例中值的 弱引用,weak reference。强引用是咱们共用 Rc<T>
实例所有权的方式。弱引用不表达所有权关系,他们的计数不会影响 Rc<T>
实例被清理的时间。他们不会引起引用循环,因为任何涉及弱引用的循环,都会在所涉及的值的强引用计数为 0
时被打破。
当咱们调用 Rc::downgrade
时,咱们会得到一个 Weak<T>
类型的灵巧指针。调用 Rc::downgrade
不是将 Rc<T>
实例中的 strong_count
增加 1
,而是将 weak_count
增加 1
。与 strong_count
类似,Rc<T>
类型使用 weak_count
来记录存在多少个 Weak<T>
引用。不同的是,在 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
的情况都得到处理,而且不会出现无效的指针。
作为一个例子,我们不是使用其项目只知道下一项目的列表数据结构,而是将创建一个其项目了解其子项目 及 其父项目的树。
创建一个树形数据结构:带有子节点的节点
Creating a Tree Data Structure: a Node with Child Nodes
首先,我们将构建一棵树,其中的节点知道他们的子节点。我们将创建一个名为 Node
的结构体,他拥有自己的 i32
值以及对其子 Node
值的引用:
文件名:src/main.rs
#![allow(unused)] fn main() { 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>
类型的值。我们还打算修改那些是另一节点的子节点的节点,因此我们在 Vec<Rc<Node>>
周围、children
字段中有一个 RefCell<T>
。
接下来,我们将使用我们的结构体定义,创建一个名为 leaf
的 Node
实例,其值为 3
,没有子节点;另一个名为 branch
的实例,其值为 5
,leaf
是其子节点之一,如下清单 15-27 所示:
文件名:src/main.rs
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)]), }); }
清单 15-27:创建一个没有子节点的 leaf
节点和一个以 leaf
作为其子节点之一的 branch
节点
我们克隆了 leaf
中的 Rc<Node>
并将其存储在 branch
中,这意味着 leaf
中的 Node
现在有两个所有者:leaf
和 branch
。我们可以通过 branch.children
从 branch
获取到 leaf
,但是没有办法从 leaf
获取到 branch
。原因是 leaf
没有对 branch
的引用,不知道他们之间的关系。我们想让 leaf
知道 branch
是他的父节点。下一步我们将这样做。
在子节点中添加到其父节点的引用
Adding a Reference from a Child to Its Parent
为了让子节点知道他的父节点,我们需要在我们的 Node
结构体定义中添加一个父节点字段。问题在于确定出父节点的类型是什么。我们知道他不能包含一个 Rc<T>
,因为这将创建一个引用循环,即 leaf.parent
指向 branch
,而 branch.children
指向 leaf
,这将导致他们的 strong_count
值永远为 0。
以另一种方式思考这些关系,一个父节点应该拥有他的子节点:如果一个父节点被弃用,他的子节点也应该被弃用。然而,一个子节点不应该拥有他的父节点:如果我们弃用某个子节点,父节点应该仍然存在。这就是弱引用的情况!
因此,我们将使用 Weak<T>
代替 Rc<T>
,具体来说是 RefCell<Weak<Node>>
。现在我们的节点结构定义如下所示:
文件名:src/main.rs
#![allow(unused)] fn main() { use std::cell::RefCell; use std::rc::Rc; #[derive(Debug)] struct Node { value: i32, parent: RefCell<Weak<Node>>, children: RefCell<Vec<Rc<Node>>>, } }
节点将能够引用其父节点但不拥有其父节点。在下面清单 15-28 中,我们更新了 main
以使用这个新定义,这样 leaf
节点将有办法引用其父节点 branch
:
文件名:src/main.rs
fn main() { let leaf = Rc::new(Node { value: 3, parent: RefCell::new(Weak::new()), children: RefCell::new(vec! []), }); println! ("叶子节点的父节点 = {:?}", 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.borrow().upgrade()); }
清单 15-28:对其父节点 branch
有弱引用的 leaf
节点
创建 leaf
节点看起来与清单 15-27 相似,除了父字段:leaf
开始时没有父节点,所以我们创建一个新的、空的 Weak<Node>
引用实例。
此时,当我们试图通过使用 upgrade
方法来获得对 leaf
的父节点的引用时,我们得到的是一个 None
值。我们在第一个 println!
语句的输出中看到了这一点:
叶子节点的父节点 = None
当我们创建 branch
节点时,他在 parent
字段中也会有一个新的 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)
:
叶子节点的父节点 = 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
和 weak_count
的变化
Visualizing Changes to strong_count
and weak_count
咱们来看看通过创建一个新的内部作用域并将 branch
的创建移到该作用域中,Rc<Node>
实例的 strong_count
和 weak_count
值如何变化。通过这样做,我们可以看到在 branch
被创建后,当他离开作用域时,会发生什么。修改部分如下清单 15-29 所示:
文件名:src/main.rs
#![allow(unused)] fn main() { {{#include ../projects/tree_demo/src/main.rs:11:}} }
清单 15-29:在内层作用域中创建 branch
并检查强引用和弱引用计数
leaf
创建后,其 Rc<Node>
的 strong count
为 1
,weak count
为 0
。
在内层作用域中,我们创建了 branch
并将其与 leaf
关联,此时当我们打印计数时, branch
中的 Rc<Node>
将有 1
的强计数和 1
的弱计数(因为 leaf.parent
指向 branch
的是 Weak<Node>
)。当我们打印 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
特质实现中。通过在 Node
的定义中指定子节点与其父节点的关系应是 Weak<T>
引用,咱们可以让父节点指向子节点,反之亦然,而不会产生引用循环和内存泄漏。
本章小结
本章介绍了如何使用灵巧指针来进行与 Rust 默认的普通引用不同的保证和取舍。Box<T>
类型有一个已知的大小,指向在内存堆上分配的数据。Rc<T>
类型记录了对内存堆上数据的引用数量,因此数据可以有多个所有者。RefCell<T>
类型及其内部可变性为我们提供了一种类型,当我们需要不可变类型但需要改变该类型的内部值时,我们可以使用这种类型;他还在运行时而不是在编译时强制执行借用规则。
我们还讨论了 Deref
和 Drop
特质,这两个特质实现了灵巧指针的很多功能。我们探讨了可能导致内存泄露的引用循环以及如何使用 Weak<T>
来防止它们。
如果本章引起了你的兴趣,并且你想实现你自己的智能指针,请查看 "The Rustonomicon" ,以获得更多有用的信息。
接下来,我们将讨论 Rust 中的并发问题。咱们甚至会了解到一些新的灵巧指针。