0


【Rust】——引用循环与内存泄漏

💻博主现有专栏:

** C51单片机(STC89C516),c语言,c++,离散数学,算法设计与分析,数据结构,Python,Java基础,MySQL,linux,基于HTML5的网页设计及应用,Rust(官方文档重点总结),jQuery,前端vue.js,Javaweb开发,Python机器学习等
🥏主页链接:**

** **Y小夜-CSDN博客

    Rust 的内存安全性保证使其难以意外地制造永远也不会被清理的内存(被称为 **内存泄漏**(*memory leak*)),但并不是不可能。Rust 并不保证完全防止内存泄漏,这意味着内存泄漏在 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() {}
    现在 
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>

引用,就能够拥有父节点和子节点之间的双向引用而不会造成引用循环和内存泄漏。


本文转载自: https://blog.csdn.net/shsjssnn/article/details/136823545
版权归原作者 Y小夜 所有, 如有侵权,请联系我们删除。

“【Rust】——引用循环与内存泄漏”的评论:

还没有评论