【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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/961524.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

[免费]基于Python的Django博客系统【论文+源码+SQL脚本】

大家好&#xff0c;我是java1234_小锋老师&#xff0c;看到一个不错的基于Python的Django博客系统&#xff0c;分享下哈。 项目视频演示 【免费】基于Python的Django博客系统 Python毕业设计_哔哩哔哩_bilibili 项目介绍 随着互联网技术的飞速发展&#xff0c;信息的传播与…

乐优商城项目总结

文章目录 项目简介微服务集群1.enreka注册中心2. zuul网关3. 公共工具类4. 商品微服务5. 文件上传微服务6. 搜索微服务7. 页面静态化微服务8. 用户微服务9. 短信微服务10. 认证微服务11. 购物车微服务12. 订单微服务项目最大的收获项目遇到的问题 项目简介 乐优商城是一个全品…

基于django的智能停车场车辆管理深度学习车牌识别系统

完整源码项目包获取→点击文章末尾名片&#xff01;

【开源免费】基于Vue和SpringBoot的在线文档管理系统(附论文)

本文项目编号 T 038 &#xff0c;文末自助获取源码 \color{red}{T038&#xff0c;文末自助获取源码} T038&#xff0c;文末自助获取源码 目录 一、系统介绍二、演示录屏三、启动教程四、功能截图五、文案资料5.1 选题背景5.2 国内外研究现状5.3 可行性分析 六、核心代码6.1 查…

双层Git管理项目,github托管显示正常

双层Git管理项目&#xff0c;github托管显示正常 背景 在写React项目时&#xff0c;使用Next.js,该项目默认由git托管。但是我有在项目代码外层记笔记的习惯&#xff0c;我就在外层使用了git托管。 目录如下 code 层内也有.git 文件&#xff0c;对其托管。 我没太在意&…

54.数字翻译成字符串的可能性|Marscode AI刷题

1.题目 问题描述 小M获得了一个任务&#xff0c;需要将数字翻译成字符串。翻译规则是&#xff1a;0对应"a"&#xff0c;1对应"b"&#xff0c;依此类推直到25对应"z"。一个数字可能有多种翻译方法。小M需要一个程序来计算一个数字有多少种不同的…

基于Langchain-Chatchat + ChatGLM 本地部署知识库

一、相关环境 参考链接: Github:https://github.com/chatchat-space/Langchain-Chatchat Langchain-chatchat版本&#xff1a;v0.3.1 安装环境&#xff1a;Ubuntu&#xff1a;22.04&#xff0c;CUDA&#xff1a;12.1 二、搭建过程 2.1 环境配置 2.1.1 创建chatchat虚拟环…

Hive:日志,hql运行方式,Array,行列转换

日志 可以在终端通过 find / | grep hive-log4j2 命令查找Hive的日志配置文件 这些文件用于配置Hive的日志系统。它们不属于系统日志也不属于Job日志&#xff0c;而是用于配置Hive如何记录系统日志和Job日志, 可以通过hive-log4j2 查找日志的位置 HQL的3种运行方式 第1种就是l…

护眼好帮手:Windows显示器调节工具

在长时间使用电脑的过程中&#xff0c;显示器的亮度和色温对眼睛的舒适度有着重要影响。传统的显示器调节方式不仅操作繁琐&#xff0c;而且在低亮度下容易导致色彩失真。因此&#xff0c;今天我想为大家介绍一款适用于Windows系统的护眼工具&#xff0c;它可以帮助你轻松调节显…

简要介绍C语言和c++的共有变量,以及c++特有的变量

在C语言和C中&#xff0c;变量是用来存储数据的内存位置&#xff0c;它们的使用方式和特性在两种语言中既有相似之处&#xff0c;也有不同之处。以下分别介绍C语言和C的共有变量以及C特有的变量。 C语言和C的共有变量 C语言和C都支持以下类型的变量&#xff0c;它们在语法和基…

Python爬虫学习第三弹 —— Xpath 页面解析 实现无广百·度

早上好啊&#xff0c;大佬们。上回使用 Beautiful Soup 进行页面解析的内容是不是已经理解得十分透彻了~ 这回我们再来尝试使用另外一种页面解析&#xff0c;来重构上一期里写的那些代码。 讲完Xpath之后&#xff0c;小白兔会带大家解决上期里百度搜索的代码编写&#xff0c;保…

消息队列篇--通信协议篇--应用层协议和传输层协议理解

在网络通信中&#xff0c;传输层协议和应用层协议是OSI模型中的两个不同层次的协议&#xff0c;它们各自承担着不同的职责。 下文中&#xff0c;我们以TCP/UDP&#xff08;传输层协议&#xff09;和HTTP/SMTP&#xff08;应用层协议&#xff09;为例进行详细解释。 1、传输层协…

Maui学习笔记- SQLite简单使用案例02添加详情页

我们继续上一个案例&#xff0c;实现一个可以修改当前用户信息功能。 当用户点击某个信息时&#xff0c;跳转到信息详情页&#xff0c;然后可以点击编辑按钮导航到编辑页面。 创建项目 我们首先在ViewModels目录下创建UserDetailViewModel。 实现从详情信息页面导航到编辑页面…

arkui-x跨平台与android java联合开发

华为鸿蒙系统采用的是arkts&#xff0c;支持跨平台crossplatform 即前端为arkts&#xff0c;arkui-x框架&#xff0c;后端为其他的语言框架。 本篇示例后端采用的是java&#xff0c;android studio工程。 主要方式是前端鸿蒙完成界面元素、布局等效果&#xff0c;后面androi…

Unity敌人逻辑笔记

写ai逻辑基本上都需要状态机。因为懒得手搓状态机&#xff0c;所以选择直接用动画状态机当逻辑状态机用。 架构设计 因为敌人的根节点已经有一个animator控制动画&#xff0c;只能增加一个子节点AI&#xff0c;给它加一个animator指向逻辑“动画”状态机。还有一个脚本&#…

ts 基础核心

吴悠讲编程 : 20分钟学会TypeScript 无废话速成TS https://www.bilibili.com/video/BV1gX4y177Kf

BGP分解实验·11——路由聚合与条件性通告(3)

续接上&#xff08;2&#xff09;的实验。其拓扑如下&#xff1a; 路由聚合的负向也就是拆分&#xff0c;在有双出口的情况下&#xff0c;在多出口做流量分担是优选方法之一。 BGP可以根据指定来源而聚合路由&#xff0c;在产生该聚合路由的范围内的条目注入到本地BGP表后再向…

【leetcode】T1599

解题心得&#xff1a; 题目长且绕&#xff0c;直接看测试样例的解析有助于更快把握题目核心需求&#xff08;即关注样例的输入、运算逻辑、输出&#xff09; 题面 原题链接1599. 经营摩天轮的最大利润 - 力扣&#xff08;LeetCode&#xff09; AC代码 class Solution { pub…

Ansible自动化运维实战--通过role远程部署nginx并配置(8/8)

文章目录 1、准备工作2、创建角色结构3、编写任务4、准备配置文件&#xff08;金甲模板&#xff09;5、编写变量6、编写处理程序7、编写剧本8、执行剧本Playbook9、验证-游览器访问每台主机的nginx页面 在 Ansible 中&#xff0c;使用角色&#xff08;Role&#xff09;来远程部…

关于opencv环境搭建问题:由于找不到opencv_worldXXX.dll,无法执行代码,重新安装程序可能会解决此问题

方法一&#xff1a;利用复制黏贴方法 打开opencv文件夹目录找到\opencv\build\x64\vc15\bin 复制该目录下所有文件&#xff0c;找到C:\Windows\System32文件夹&#xff08;注意一定是C盘&#xff09;黏贴至该文件夹重新打开VS。 方法二&#xff1a;直接配置环境 打开opencv文…