首页 > 系统相关 >引用循环与内存泄漏

引用循环与内存泄漏

时间:2024-04-16 19:56:16浏览次数:16  
标签:count 泄漏 leaf 内存 branch Rc RefCell 引用

Rust 的内存安全性保证使其难以意外地制造永远也不会被清理的内存(被称为 内存泄漏memory leak)),但并不是不可能。Rust 并不保证完全防止内存泄漏,这意味着内存泄漏在 Rust 中被认为是内存安全的。这一点可以通过 Rc<T> 和 RefCell<T> 看出:创建引用循环的可能性是存在的。这会造成内存泄漏,因为每一项的引用计数永远也到不了 0,持有的数据也就永远不会被释放。

制造引用循环

让我们看看引用循环是如何发生的以及如何避免它。以示例 15-25 中的 List 枚举和 tail 方法的定义开始:

文件名:src/main.rs

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() {}

示例 15-25: 一个存放 RefCell 的 cons list 定义,这样可以修改 Cons 成员所引用的数据

这里采用了示例 15-5 中 List 定义的另一种变体。现在 Cons 成员的第二个元素是 RefCell<Rc<List>>,这意味着不同于像示例 15-24 那样能够修改 i32 的值,我们希望能够修改 Cons 成员所指向的 List。这里还增加了一个 tail 方法来方便我们在有 Cons 成员的时候访问其第二项。

在示例 15-26 中增加了一个 main 函数,其使用了示例 15-25 中的定义。这些代码在 a 中创建了一个列表,一个指向 a 中列表的 b 列表,接着修改 a 中的列表指向 b 中的列表,这会创建一个引用循环。在这个过程的多个位置有 println! 语句展示引用计数。

文件:src/main.rs

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());
}

 

from: 引用循环与内存泄漏

示例 15-26:创建一个引用循环:两个 List 值互相指向彼此

 

这里在变量 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>

 

如果保持最后的 println! 行注释并运行代码,会得到如下输出:

$ cargo run
   Compiling cons-list v0.1.0 (file:///projects/cons-list)
    Finished dev [unoptimized + debuginfo] target(s) in 0.53s
     Running `target/debug/cons-list`
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

可以看到将列表 a 修改为指向 b 之后, a 和 b 中的 Rc<List> 实例的引用计数都是 2。在 main 的结尾,Rust 丢弃 b,这会使 b Rc<List> 实例的引用计数从 2 减为 1。然而,b Rc<List> 不能被回收,因为其引用计数是 1 而不是 0。接下来 Rust 会丢弃 a 将 a Rc<List> 实例的引用计数从 2 减为 1。这个实例也不能被回收,因为 b Rc<List> 实例依然引用它,所以其引用计数是 1。这些列表的内存将永远保持未被回收的状态。为了更形象的展示,我们创建了一个如图 15-4 所示的引用循环:

Reference cycle of lists

图 15-4: 列表 a 和 b 彼此互相指向形成引用循环

如果取消最后 println! 的注释并运行程序,Rust 会尝试打印出 a 指向 b 指向 a 这样的循环直到栈溢出。

相比真实世界的程序,这个例子中创建引用循环的结果并不可怕。创建了引用循环之后程序立刻就结束了。如果在更为复杂的程序中并在循环里分配了很多内存并占有很长时间,这个程序会使用多于它所需要的内存,并有可能压垮系统并造成没有内存可供使用。

创建引用循环并不容易,但也不是不可能。如果你有包含 Rc<T> 的 RefCell<T> 值或类似的嵌套结合了内部可变性和引用计数的类型,请务必小心确保你没有形成一个引用循环;你无法指望 Rust 帮你捕获它们。创建引用循环是一个程序上的逻辑 bug,你应该使用自动化测试、代码评审和其他软件开发最佳实践来使其最小化。

另一个解决方案是重新组织数据结构,使得一部分引用拥有所有权而另一部分没有。换句话说,循环将由一些拥有所有权的关系和一些无所有权的关系组成,只有所有权关系才能影响值是否可以被丢弃。在示例 15-25 中,我们总是希望 Cons 成员拥有其列表,所以重新组织数据结构是不可能的。让我们看看一个由父节点和子节点构成的图的例子,观察何时是使用无所有权的关系来避免引用循环的合适时机。

避免引用循环:将 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

文件名:src/main.rs

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,如示例 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 现在有两个所有者:leafbranch。可以通过 branch.children 从 branch 中获得 leaf,不过无法从 leaf 到 branchleaf 没有到 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 引用其父节点:

文件名:src/main.rs

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());
}

示例 15-28:一个 leaf 节点,其拥有指向其父节点 branch 的 Weak 引用

创建 leaf 节点类似于示例 15-27,除了 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 和 weak_count 的改变

让我们通过创建了一个新的内部作用域并将 branch 的创建放入其中,来观察 Rc<Node> 实例的 strong_count 和 weak_count 值的变化。这会展示当 branch 创建和离开作用域被丢弃时会发生什么。这些修改如示例 15-29 所示:

文件名:src/main.rs

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),
    );
}

示例 15-29:在内部作用域创建 branch 并检查其强弱引用计数

一旦创建了 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>引用,就能够拥有父节点和子节点之间的双向引用而不会造成引用循环和内存泄漏。

总结

这一章涵盖了如何使用智能指针来做出不同于 Rust 常规引用默认所提供的保证与取舍。Box<T> 有一个已知的大小并指向分配在堆上的数据。Rc<T> 记录了堆上数据的引用数量以便可以拥有多个所有者。RefCell<T> 和其内部可变性提供了一个可以用于当需要不可变类型但是需要改变其内部值能力的类型,并在运行时而不是编译时检查借用规则。

我们还介绍了提供了很多智能指针功能的 trait Deref 和 Drop。同时探索了会造成内存泄漏的引用循环,以及如何使用 Weak<T> 来避免它们。

如果本章内容引起了你的兴趣并希望现在就实现你自己的智能指针的话,请阅读 “The Rustonomicon” 来获取更多有用的信息。

接下来,让我们谈谈 Rust 的并发。届时甚至还会学习到一些新的对并发有帮助的智能指针。

 

标签:count,泄漏,leaf,内存,branch,Rc,RefCell,引用
From: https://www.cnblogs.com/guxuanqing/p/18139048

相关文章

  • rust引用计数智能指针Rc<T>
    大部分情况下所有权是非常明确的:可以准确地知道哪个变量拥有某个值。然而,有些情况单个值可能会有多个所有者。例如,在图数据结构中,多个边可能指向相同的节点,而这个节点从概念上讲为所有指向它的边所拥有。节点在没有任何边指向它从而没有任何所有者之前,都不应该被清理掉。为了启用......
  • 关于内存泄漏和代码优化
    最近在用selenium对网页上的数据进行爬取时发现,通过pyinstaller打包成的.exe文件运行时速度越来越慢,通过查找任务管理器里的句柄数,发现可能存在内存泄漏的原因,记录一下关于内存泄漏的处理方法和代码优化1.内存泄漏如果您怀疑代码可能存在内存泄漏,可以尝试跟踪内存分配:这里我用......
  • C++动态内存分配/malloc/new
    0前言这部分确实是面试老八股了,不过我还是记录一下1内存分区在C语言中,将内存分为程序代码区+数据区,其中数据区又分为静态存储区和动态存储区在C++中,分为五种:动态存储区:栈区:存放局部变量,由编译器自动分配释放,程序员不能操作堆:由程序员使用malloc/new申请,用free/delete......
  • hive on spark内存模型
    内容介绍hiveonspark的调优,那必然涉及到这一系列框架的内存模型。本章就是来讲一下这些框架的内存模型。hiveonspark的任务,从开始到结束。总共涉及了3个框架。分别是:yarn、hive、spark其中,hive只是一个客户端的角色。就不涉及任务运行时的内存。所以这里主要讲的yarn和spa......
  • influxdb内存占用剖析
    目标本文将讲述influxdb内存中都存了哪些数据?什么情况下会导致内存占用暴增?以及内存相关的配置。我的influxdb配置我的influxdb配置如下:/etc/influxdb/config.tomlbolt-path="/var/lib/influxdb/influxd.bolt"engine-path="/var/lib/influxdb/engine"storage-cache-m......
  • C++ 默认参数与引用传递:语法、用法及示例
    C++默认参数默认参数概述在C++中,函数参数可以拥有默认值。这意味着,在调用函数时,如果省略了某个参数,那么将使用为该参数指定的默认值。设置默认参数默认参数值使用等号=符号进行设置,位于参数声明的类型之后。例如:voidmyFunction(stringcountry="Norway");在这个例......
  • C++_内存模型和函数以及类
    C++内存模型函数函数与编译器类成员变量class内部通过 static修饰变量时,表示该变量为静态成员变量,必须在类外定义 staticconst修饰变量时,表示该变量为静态成员常量,可以在类内初始化,或者在类外初始化 staticconstexpr修饰变量时,表示该......
  • C++对象封装后的内存布局
    在C语言中,数据和数据的处理操作(函数)是分开声明的,在语言层面并没有支持数据和函数的内在关联性,我们称之为过程式编程范式或者程序性编程范式。C++兼容了C语言,当然也支持这种编程范式。但C++更主要的特点在支持基于对象(object-based,OB)和面向对象(object-oriented,OO),OB和OO的基础是......
  • mongo内存管理之cache占用过高,影响正常业务
    云数据库DDS实例shard连接数异常激增影响业务的事件,问题分析如下保障现象:在当天22日21:50-22:21时间段内,连接数异常激增,同时伴随有大量慢日志,实例整体响应速度下降。3月23日19:35贵司报障该实例再次出现shard连接数激增的现象。排查处理:经过排查发现3月22日21:50-......
  • Go实践:用Sync.Map实现简易内存缓存系统
    介绍定义了一个Cache结构体,其中使用sync.Map作为底层数据结构来存储缓存项。Set方法用于设置缓存项,指定键、值以及过期时间。Get方法用于获取缓存项,如果缓存项存在且未过期,则返回值和true,否则返回nil和false。方法的接受者为指针类型,是为了对Cache对象进行操作,并在方法内部访问和......