【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.rhkb.cn/news/9454.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

免杀国内主流杀软的恶意样本分析

目录下存在愤怒的小鸟.exe和fun.dll文件&#xff0c;最新版火绒&#xff0c;windows defender&#xff0c;腾讯电脑管家&#xff0c;360静态扫描都未发现恶意程序 动态执行&#xff0c;杀软也未拦截 上传到virustotal网站分析恶意程序&#xff0c;只有三个引擎检测出来 die分析…

gesp(C++六级)(6)洛谷:P10109:[GESP202312 六级] 工作沟通

gesp(C六级)&#xff08;6&#xff09;洛谷&#xff1a;P10109&#xff1a;[GESP202312 六级] 工作沟通 题目描述 某公司有 N N N 名员工&#xff0c;编号从 0 0 0 至 N − 1 N-1 N−1。其中&#xff0c;除了 0 0 0 号员工是老板&#xff0c;其余每名员工都有一个直接领导…

16届蓝桥杯寒假刷题营】第2期DAY5IOI赛

3.小蓝小彬的代码挑战 - 蓝桥云课 问题描述 在蓝桥杯大赛中&#xff0c;小蓝和小彤是一对好朋友。他们在比赛中遇到了一个有趣的挑战。这个挑战是给定一个由大写字母组成的代码&#xff0c;他们需要找出这串代码中有多少个子序列LQB。小蓝和小彬都很聪明&#xff0c;他们想到…

【云安全】云原生-K8S-搭建/安装/部署

一、准备3台虚拟机 务必保证3台是同样的操作系统&#xff01; 1、我这里原有1台centos7&#xff0c;为了节省资源和效率&#xff0c;打算通过“创建链接克隆”2台出来 2、克隆之前&#xff0c;先看一下是否存在k8s相关组件&#xff0c;或者docker相关组件 3、卸载原有的docker …

【JavaEE】_MVC架构与三层架构

目录 1. MVC架构 2. 三层架构 3. MVC架构与三层架构的对比 3.1 MVC与三层架构的对比 3.2 MVC与三层架构的共性 1. MVC架构 在前文已介绍关于SpringMAC的设计模式&#xff0c;详见下文&#xff1a; 【JavaEE】_Spring Web MVC简介-CSDN博客文章浏览阅读967次&#xff0c;点…

基于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虚拟环…

【win11】解决msrdc.exe窗口启动导致周期性失去焦点

msrdc.exe randomly stealing focus 最近写代码时&#xff0c;经常出现周期性失去焦点的情况非常恼人,不确定是否是Q4的微软更新引起&#xff1f;换了输入法也不行&#xff0c;只能借助工具来查看&#xff1a;大神的工具非常好&#xff1a; 可以发现是remote app 启动了一个UI…

危机13小时:追踪一场GitHub投毒事件

事件概要 自北京时间 2024.12.4 晚间6点起&#xff0c; GitHub 上不断出现“幽灵仓库”&#xff0c;仓库中没有任何代码&#xff0c;只有诱导性的病毒文件。当天&#xff0c;他们成为了 GitHub 上 star 增速最快的仓库。超过 180 个虚假僵尸账户正在传播病毒&#xff0c;等待不…

论文阅读(十三):复杂表型关联的贝叶斯、基于系统的多层次分析:从解释到决策

1.论文链接&#xff1a;Bayesian, Systems-based, Multilevel Analysis of Associations for Complex Phenotypes: from Interpretation to Decision 摘要&#xff1a; 遗传关联研究&#xff08;GAS&#xff09;报告的结果相对稀缺&#xff0c;促使许多研究方向。尽管关联概念…

设计模式-建造者模式、原型模式

目录 建造者模式 定义 类图 优缺点 角色 建造者模式和工厂模式比较 使用案例 原型模式 定义 类图 优缺点 应用场景 应用类型 浅克隆 深克隆 建造者模式 定义 将一个复杂的对象的构造与它的表示分离&#xff0c;使同样的构建过程可以创建不同的表示&#xff0c;…

Python GUI 开发 | Qt Designer — 工具介绍

关注这个框架的其他相关笔记&#xff1a;Python GUI 开发 | PySide6 & PyQt6 学习手册-CSDN博客 Qt Designer 即 Qt 设计师&#xff0c;是一个强大、灵活的可视化 GUI 设计工具&#xff0c;可以帮助用户加快开发 PySide6 程序的速度。 Qt Designer 是专门用来制作 PySide6…

园区管理智能化创新引领企业效能提升与风险控制新趋势

内容概要 在现代园区管理中&#xff0c;智能化创新正成为越来越多企业优化效能和控制风险的重要途径。通过引入先进的技术手段&#xff0c;企业能够更高效地管理资源&#xff0c;并实现全面的风险控制。 首先&#xff0c;园区管理系统的基本概念和发展现状让我们看到科技与管…

LTV预估 | 深度学习PLTV之开山鼻祖ZILN

&#x1f923; 这一集让我们欢迎基于深度学习的pltv方法&#xff1a;ZILN&#xff0c;ZILN可以说是后面很多研究的参考方法&#xff0c;我看了好几篇最新的pltv论文&#xff0c;都是基于ZILN来做的。 文章目录 1 精简总结2 背景&挑战&#xff1a;3 方法&#xff1a;实验&am…

docker安装emqx

emqx安装 拉取emqx镜像 docker pull emqx/emqx:v4.1.0 运行docker容器 docker run -tid --name emqx -p 1883:1883 -p 8083:8083 -p 8081:8081 -p 8883:8883 -p 8084:8084 -p 18083:18083 emqx/emqx:v4.1.0 放行端口 1、如果要是自己的虚拟机&#xff0c;并且关闭了防火墙&a…

【C++题解】1393. 与7无关的数?

欢迎关注本专栏《C从零基础到信奥赛入门级&#xff08;CSP-J&#xff09;》 问题&#xff1a;1393. 与7无关的数&#xff1f; 类型&#xff1a;简单循环 题目描述&#xff1a; 一个整数&#xff0c;如果这个数能够被 7 整除&#xff0c;或者其中有一位是7&#xff0c;我们称…

分享| RL-GPT 框架通过慢agent和快agent结合提高AI解决复杂任务的能力-Arxiv

结论 “RL-GPT: Integrating Reinforcement Learning and Code-as-policy” RL-GPT 框架为解决大语言模型在复杂任务处理中的难题提供了创新有效的途径&#xff0c; 旨在将强化学习&#xff08;RL&#xff09;和代码即策略相结合&#xff0c; 以解决大语言模型&#xff08…

多级缓存(亿级并发解决方案)

多级缓存&#xff08;亿级流量&#xff08;并发&#xff09;的缓存方案&#xff09; 传统缓存的问题 传统缓存是请求到达tomcat后&#xff0c;先查询redis&#xff0c;如果未命中则查询数据库&#xff0c;问题如下&#xff1a; &#xff08;1&#xff09;请求要经过tomcat处…

3、C#基于.net framework的应用开发实战编程 - 实现(三、三) - 编程手把手系列文章...

三、 实现&#xff1b; 三&#xff0e;三、编写应用程序&#xff1b; 此文主要是实现应用的主要编码工作。 1、 分层&#xff1b; 此例子主要分为UI、Helper、DAL等层。UI负责便签的界面显示&#xff1b;Helper主要是链接UI和数据库操作的中间层&#xff1b;DAL为对数据库的操…

RPC是什么?和HTTP区别?

RPC 是什么&#xff1f;HTTP 是什么&#xff1f; 作为一个程序员&#xff0c;假设我们需要从A电脑的进程发送一段数据到B电脑的进程&#xff0c;我们一般会在代码中使用 Socket 进行编程。 此时&#xff0c;可选性一般就是 TCP 和 UDP 二选一&#xff0c;由于 TCP 可靠、UDP 不…

在无sudo权限Linux上安装 Ollama 并使用 DeepSeek-R1 模型

本教程将指导你如何在 Linux 系统上安装 Ollama&#xff08;一个本地运行大型语言模型的工具&#xff09;&#xff0c;并加载 DeepSeek-R1 模型。DeepSeek-R1 是一个高性能的开源语言模型&#xff0c;适用于多种自然语言处理任务。 DeepSeek-R1 简介 DeepSeek-R1 是 DeepSeek …