当前位置: 首页 > article >正文

【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的方法,用于获取ListCons变体附带的第二个元素,如果有就返回其值,用Some封装,是Nil就返回None

  • 然后在main函数创建了ab两个List的实例,b内部共享了a的值。这种链表的代码看着就犯恶心,所以我把其结构图放在请添加图片描述
    这里:

  • main函数里还通过Rc::strong_count获取了ab的强引用数量,使用自定义的tail方法获了Cons附带的第二个元素,用println!打印出来。

  • 下面使用if let语句把aCons的第二个值绑在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的第二个元素指向bb的引用数量加1变为2。此时a指向了bb又指向了a,就会造成循环引用。

ab都走出了作用域,Rust删除了变量b ,这将b的引用计数从 2 减少到 1。此时Rc<List>在堆上的内存不会被删除,因为它的引用计数是1,而不是0。然后 Rust 删除a ,这会将aRc<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节点有两个所有者。目前可以通过branchchildren字段访问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被创建后打印了leafparent字段内容(这时其内容就是branch)。

*leaf.parent.borrow_mut() = Rc::downgrade(&branch);这句话把branch的内容从Rc<Node>变为Weak<Node>,指向了leafparent字段:

  • leaf.parent是表示leaf父节点的字段,其类型是RefCell<Weak<Node>>,所以可以使用borrow_mut来获得其可变引用&mut RefMut<Weak<Node>>
  • 使用解引用符号*把可变引用&mut RefMut<Weak<Node>>变为RefMut<Weak<Node>>
  • 通过downgrade方法把branchRc<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父节点的操作放到里面
    • 打印branchleaf在此时强引用、弱引用的数量
  • 走出作用域后:

    • 打印leafparent
    • 打印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已经走出其作用域,所以leafparent字段此时就为None
  • 第5行:branch已经走出其作用域导致它对leaf的强引用失效,leaf的强引用减1变为1

http://www.kler.cn/a/522711.html

相关文章:

  • 【Rust自学】15.7. 循环引用导致内存泄漏
  • 【Jave全栈】Java与JavaScript比较
  • hive:数据导入,数据导出,加载数据到Hive,复制表结构
  • Airflow:精通Airflow任务依赖
  • 为AI聊天工具添加一个知识系统 之76 详细设计之17 正则表达式 之4 正则表达式模板
  • 【学习笔记】计算机网络(二)
  • Spring AOP原理
  • 智能门锁开发系列:从设计到实现的全面解析
  • Mybaties缓存机制
  • 装饰SpringMVC的适配器实现响应自动包装
  • 每日一题 429. N 叉树的层序遍历
  • WebPages 表单:设计与实现指南
  • react-bn-面试
  • 使用国内镜像加速器解决 Docker Hub 拉取镜像慢或被屏蔽的问题
  • 学习第七十六行
  • 备份与恢复管理系统深度解析及代码实践
  • GitHub 仓库的 Archived 功能详解:中英双语
  • 炫酷JavaScript文本时钟
  • 跨平台物联网漏洞挖掘算法评估框架设计与实现项目后期研究方案
  • 008 mybatis缓存
  • 全志 视频输入组件的使用
  • 强化学习在自动驾驶中的实现与挑战
  • RocketMQ优势剖析-性能优化
  • 安卓入门四十二 过渡动画
  • RAG制作客服机器人,文档用表格还是QA问答对?
  • python 一个组合问题: