Rust-泄漏

在C++中,如果引用计数智能指针出现了循环引用,就会导致内存泄漏。而Rust中也一样存在引用计数智能指针Rc,那么Rust中是否可能制造出内存泄漏呢?

内存泄漏

首先,我们设计一个Node类型,它里面包含一个指针,可以指向其他的Node实例:

在这里插入图片描述
接下来我们尝试一下创建两个实例,将它们首尾相连:

在这里插入图片描述
到这里写不下去了,Rust中要求,Box指针必须被合理初始化,而初始化Box的时候又必须先传入一个Node实例,这个Node的实例又要求创建一个Box指针。这成了“鸡生蛋蛋生鸡”的无限循环。

要打破这个循环,我们需要使用“可空的指针”。在初始化Node的时候,指针应该是“空”状态,后面再把它们连接起来。我们把代码改进,为了能修改node的值,还需要使用mut:

在这里插入图片描述
编译,发生错误:“error:use of moved value:node2”。

从编译信息中可以看到,在node1.next =Some(node2);这条语句中发生了move语义,从此句往后,node2变量的生命周期已经结束了。

因此后面一句中使用node2的时候发生了错误。

那我们需要继续改进,不使用node2,换而使用node1.next,代码改成下面这样:

在这里插入图片描述
编译又发生了错误,错误信息为:“error:use of partially moved value:node1”。

这是因为在match语句中,我们把node1.next的所有权转移到了局部变量n中,这个n实际上就是node2的实例,在执行赋值操作n.next =Some(node1)的过程中,编译器认为此时node1的一部分已经被转移出去了,它不能再被用于赋值号的右边。

看来,这是因为我们选择使用的指针类型不对,Box类型的指针对所管理的内存拥有所有权,只使用Box指针没有办法构造一个循环引用的结构出来。于是,我们想到使用Rc指针。同时,我们还用了Drop trait来验证这个对象是否真正被释放了:

在这里插入图片描述
编译依然没有通过,错误信息为:“error:partial reinitialization of uninitialized structure’node2`”,还是没有达到目的。继续改进,我们将原来“栈”上分配内存改为在“堆”上分配内存:

在这里插入图片描述
编译再次不通过,错误信息为:“error:cannot assign to immutable field”。

通过这个错误信息,我们现在应该能想到,Rc类型包含的数据是不可变的,通过Rc指针访问内部数据并做修改是不可行的,必须用RefCell把它们包裹起来才可以。

继续修改:

在这里插入图片描述
因为我们使用了RefCell,对Node内部数据的修改不再需要mut关键字。编译通过,执行,这一次屏幕上没有打印任何输出,说明了析构函数确实没有被调用。

至此,终于实现了使用Rc指针构造循环引用,制造了内存泄漏。

虽然构造循环引用非常复杂,但是可能性还是存在的,Rust无法从根本上避免内存泄漏。

通过循环引用构造内存泄漏,需要同时满足三个条件:

  1. 使用引用计数指针;
  2. 存在内部可变性;
  3. 指针所指向的内容本身不是’static的。

当然,这个示例也说明,通过构造循环引用来制造内存泄漏是比较复杂的,不是轻而易举就能做到的。

构造循环引用的复杂性可能也刚好符合我们的期望,毕竟从设计原则上来说:鼓励使用的功能应该设计得越易用越好;不鼓励使用的功能,应该设计得越难用越好。

Easy to Use,Easy to Abuse.
对于上面这个例子,要想避免内存泄漏,需要程序员手动把内部某个地方的Rc指针替换为std::rc::Weak弱引用来打破循环。这是编译器无法帮我们静态检查出来的。

内存泄漏属于内存安全

在编程语言设计这个层面,“内存泄漏”是一个基本上无法在编译阶段彻底解决的问题。在许多场景下,什么是“内存泄漏”、什么不是“内存泄漏”,本身就没有一个完全客观的评判标准。

它实质上跟程序员的“意图”有关。程序很难自动判定出哪些变量是以后还会继续用的,哪些是不再被使用的。

即便是在使用GC做内存管理的环境下,程序员也有可能不小心将不应该被使用的变量错误引用,造成无法自动回收的问题。

因为GC判定一个对象是否可回收的标准是,这个对象有没有被“根”对象直接或者间接引用。

假如一个对象本来是应该被释放的,可是因为逻辑问题,没有把指向它的有效引用全部释放,那么GC依旧把它判定为不可回收。

我们可能在不经意的情况下,造成了不再需要继续使用的对象被生命周期更长的对象所引用。

面对这样的情况,GC也会显得无能为力。比如,在android编程领域,我们可能会在注册回调函数的时候把一个较大的activity引用传递过去,结果activity应该被销毁的时候,由于还有其他变量继续持有指向它的引用,从而导致该activity变量无法正常被释放,这种现象被称为“Context泄漏”。

解决这个问题的办法只能是,在必要的地方使用“弱引用”(WeakReference),避免“强引用”对变量生命周期的影响。

解决引用计数的循环引用的办法与此类似,也是一样用“弱引用”来打破循环,避免“强引用”对生命周期的影响。

再比如在javascript中注册一个定时器,而定时器不小心引用了许多大对象,这些对象会随着闭包加入到主事件循环队列中,也会造成类似的结果。

在绝大部分情况下,GC给我们带来了方便。但是,程序员也千万不能因为有GC的辅助,而忽略对变量的生命周期的设计考量。

在C++和Rust中是一样的,如果出现了循环引用,那么只能通过手动打破循环的方式来解决内存泄漏的问题。编译器无法通过静态检查来保证你不会犯这个错误。

内存泄漏显然是一种bug。但它跟“内存不安全”这种bug的性质不一样。“内存泄漏”是对“正常数据”的“应该执行但是没有执行”的操作,“内存不安全”是对“不正常数据”的“不应该执行但是执行了”的操作。

“内存不安全”导致的后果比“内存泄漏”要严重得多。

在这里插入图片描述

语言的设计者当然是希望能彻底解决内存泄,但是很可惜,这个问题恐怕不是在语言层面能彻底解决的问题。

所谓“彻底解决”的意思是,用户无论使用何种技巧,永远无法构造出内存泄漏的情况。

Rust语言无法给出这样的保证。
内存泄漏当然是不好的事情,作为开发者,我们应该尽可能避免内存泄漏现象的发生。

然而,需要强调的是,内存泄漏不属于内存安全的范畴,Rust也不会在语言设计层面给出一个“免除内存泄漏”的承诺。

析构函数泄漏

上面的例子展现了如何在Rust中不使用unsafe代码制造内存泄漏。在Rust中,在不经意间不小心制造内存泄漏的可能性是很低的。但是这个可能性还是存在的。

然而,内存泄漏并非最可怕的情况,因为内存泄漏只造成资源浪费,毕竟没有造成野指针等更为严重的内存安全问题。

上面的例子实际上还暗示了另外一种危险性,即析构函数泄漏。

在Rust中,RAⅡ手法用得非常普遍,它实际上要求程序的正确性依赖于析构函数的确定性调用。然而让我们担心的事情是,析构函数是有可能永远不会被调用的。

除了前面展示的通过循环引用导致的析构函数泄漏之外,还有许多种方式可以产生同样的效果。

比如,我们构造两个首尾相连的channel,发送端和接收端连到一起,那么在这两个channel里面传递的对象就进入了死循环,就永远不会被析构了。

析构函数泄漏是比内存泄漏更严重的情况。因为析构函数是可以“自定义”的,析构函数里面可能调用“任意的”代码。

我们一直在强调,Rust给了我们一个非常强的保证,即“内存安全”。这个保证是非常严肃认真的。

这个保证意味着,只要不使用unsafe,用户永远无法构造出“内存不安全”的情况。

然而,对于泄漏问题,Rust做不到像内存安全这种程度的保证。

所以,Rust设计者不得不痛苦地承认,析构函数并不能被保证调用。

大家不要误解了这段话,这并不是意味着Rust会轻轻松松、时时刻刻造成泄漏,它只是意味着,编译器没办法自动检查出所有可能的资源泄漏问题,并给出编译错误或警告。

承认析构函数可能不会被调用(即便在不使用unsafe代码情况下),并不会造成特别严重的问题——除非它违反了“内存安全”。

“内存安全”一直是Rust坚持的原则和底线,这条原则是永远不能被破坏的,否则Rust就失去了存在的意义。

这个结论直接导致了下面几个比较重要的后果。

其一,标准库中的std::mem::forget函数去掉了unsafe标记。

其二,允许带有析构函数的类型,作为static变量和const变量。全局变量的析构函数最后是泄漏掉了的,不会被调用。以前曾经规定带析构函数的类型不允许作为全局变量,后来放宽了规定,允许作为全局变量,但是析构函数无法调用。

其三,标准库中不安全代码需要依赖析构函数调用的逻辑得到修改,其中涉及Vec::drain_range和Thread::scoped等方法。

Rust标准库中有一个std::mem::forget函数,这个方法的签名是fn forget(t:T)。它接受的参数不是引用类型,而是将参数move进入函数体中,类似于std::mem::drop。

但它与drop最大的区别是,它会阻止编译器调用这个变量的析构函数,也不会释放它在堆上申请的内存。

它的作用就是制造泄漏。原来这个函数是unsafe的,但是,当设计者发现完全可以用安全代码写一个同样效果的forget函数,那么,它的unsafe标记也就没有什么意义了。

因此,大家决定,去掉forget函数前面的unsafe标记。这个函数不再被标记为unsafe,只是因为设计者意识到了泄漏并非内存安全问题,unsafe关键字只能用于标记跟内存安全相关的问题,并非意味着鼓励用户随意使用这个函数。那么它有什么用呢?我们可以举几个例子:

  • 我们有一个未初始化的值,我们不希望它执行析构函数;
  • 在用FFI跟外部函数打交道的时候。

即便析构函数泄漏,也不应造成内存不安全。这个结论,直接导致了thread::scoped函数从标准库中移除。

scoped函数是这样设计的:scoped函数可以创建一个线程,跟spawn函数不一样,它保证在当前函数退出以前,这个线程必定已经退出。

这样一来,我们就可以直接使用引用&来读父线程读局部变量,或者用&mut来写局部变量,避免了Arc的运行效率损失,是非常有用的scoped函数与spawn函数的区别就在于,它保证子线程一定会在当前函数退出之前退出,所以它的生命周期比当前函数的生命周期短。

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

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

相关文章

C++类与对象【友元】

🌈个人主页:godspeed_lucip 🔥 系列专栏:C从基础到进阶 🎄1 友元🥑1.1 全局函数做友元🥑1.2 类做友元🥑1.3 成员函数做友元 🕮2 总结 🎄1 友元 生活中你的家…

xml裁剪标注目标并外扩

import glob import xml.etree.ElementTree as ET import cv2 from PIL import Image import os def change_xmlfile(path)

HCIA-H12-811题目解析(11)

1、下列哪个属性不能作为衡量COST的参数? 2、RSTP协议使用P/A机制加快了上游端口转到Forwarding状态的速度,但是却没有出现临时环路的原因是什么? 3、网络管理员在三层交换机上创建了VLAN10,并在该VLAN的虚拟接口下配置了IP地址…

EtherNet/IP协议开发2:在ubuntu测试

下载源码: git clone https://github.com/EIPStackGroup/OpENer编译 首先进入目录 /big/opener/OpENer/bin/posix 执行脚本: lkmaoubuntu:/big/opener/OpENer/bin/posix$ ./setup_posix.sh 执行make lkmaoubuntu:/big/opener/OpENer/bin/posix$ mak…

基于arcgis js api 4.x开发点聚合效果

一、代码 <html> <head><meta charset"utf-8" /><meta name"viewport"content"initial-scale1,maximum-scale1,user-scalableno" /><title>Build a custom layer view using deck.gl | Sample | ArcGIS API fo…

SPEC CPU 2017 quick start

SPEC CPU 2017 quick start 我这里选择在 linux&#xff08;ubuntu22.04.3&#xff09; 上安装 SPEC CPU 2017&#xff0c;gcc、g、gfortran 均使用 sudo apt install xxx 安装&#xff08;其版本为11.4.0&#xff09; 官方的 SPEC CPU 2017 在 Unix Systems 安装示例&#x…

2024-01-18 在Android Studio中,可以通过修改build.gradle文件(位于你的应用模块目录下)来自定义生成的APK名称

一、在Android Studio中&#xff0c;可以通过修改build.gradle文件&#xff08;位于你的应用模块目录下&#xff09;来自定义生成的APK名称&#xff0c;在build.gradle里面增加下面的代码 applicationVariants.all { variant ->variant.outputs.all {outputFileName "…

349. 两个数组的交集(力扣)(OJ题)

题目链接&#xff1a;349. 两个数组的交集 - 力扣&#xff08;LeetCode&#xff09; 个人博客主页&#xff1a;https://blog.csdn.net/2301_79293429?typeblog 专栏&#xff1a;https://blog.csdn.net/2301_79293429/category_12545690.html 给定两个数组 nums1 和 nums2 &a…

json-server的基础使用

json-server 是什么? 用来快速搭建模拟的 REST API 的工具包 可以30秒内快速为我们搭建一个假的基于 REST API的服务 我们要如何使用呢&#xff1f; 1.先安装 //全局安装 npm i -g json-server 2.创建文件 db.json 我们需要在db.json放入一点内容 放入示例&#xff1a; {/…

用 Python 制作可视化 GUI 界面,一键实现证件照背景颜色的替换

今天&#xff0c;我们来分享一下如何通过Python的十来行代码来替换证件照的背景颜色&#xff0c;那么在最后&#xff0c;小编也会将上述的流程制作成一个GUI界面来方便大家使用。关于界面的大致模样其实和先前的相差不大&#xff0c;大家应该都看过上一篇的内容 界面大体的样子…

Python项目——久坐提醒定时器(PySide6)编写

1、介绍 使用Python编写一个久坐提醒软件。功能&#xff1a; 设置工作时间。设置休息时间。选择休息时是否播放音乐。休息时&#xff0c;软件置顶&#xff0c;且不能关闭。 2、工具 语言&#xff1a;python3.11UI设计工具&#xff1a;Qt designer编译器&#xff1a;PyCharm包…

北斗卫星:助力社区矫正人员追踪与管理的科技突破

北斗卫星&#xff1a;助力社区矫正人员追踪与管理的科技突破 社区矫正人员是一个重要的社会群体&#xff0c;他们的安全和管理对于社会的和谐稳定至关重要。随着技术的飞跃发展&#xff0c;北斗卫星系统作为我国自主研发的卫星导航系统&#xff0c;正逐渐在社区矫正工作中发挥…

《世界之外》提前开测,网易打响国乙大战

1月18日&#xff0c;国乙市场迎来了一场大战。 原定于1月26日开服的网易新乙游《世界之外》&#xff0c;突然宣布在1月18日进行不删档、不限量测试&#xff0c;从某种意义上来说&#xff0c;其实就等同于提前公测。 而同一天开服的还有叠纸的全新3D乙游《恋与深空》&#xff…

比特币狂人引爆达沃斯论坛

点击查看TechubNews原文链接&#xff1a;比特币狂人引爆达沃斯论坛 比特币狂人、自称无政府资本主义者的阿根廷总统米莱在达沃斯的最新演讲引爆社交网络大讨论。 1 月 15 日&#xff0c;第 54 届世界经济论坛在瑞士阿尔卑斯山的达沃斯开幕。来自约 60 个国家首脑和跨国公司的领…

音乐人声分离工具:极简的人声和背景音乐分离工具

这是一个极简的人声和背景音乐分离工具&#xff0c;本地化网页操作&#xff0c;无需连接外网&#xff0c;使用 2stems/4stems/5stems 模型。 将一首歌曲或者含有背景音乐的音视频文件&#xff0c;拖拽到本地网页中&#xff0c;即可将其中的人声和音乐声分离为单独的音频wav文件…

编程笔记 html5cssjs 045 网页布局

编程笔记 html5&css&js 045 网页布局 一、网页布局二、头部区域三、菜单导航区域三、内容区域四、不相等的列五、底部区域六、box-sizingbox-sizing 属性可以被用来调整这些表现&#xff1a;属性值content-boxborder-box 六、响应式网页布局小结 网页布局有很多种方式&…

MySQL(视图,存储函数,存储过程)

作业1&#xff1a; 作业实现&#xff1a; 首先创建学生表&#xff0c;课程表&#xff0c;以及学生选课表。 CREATE TABLE Student (Sno INT PRIMARY KEY,Sname VARCHAR(20) NOT NULL,Ssex CHAR(1) CHECK (Ssex IN (男, 女)),Sage INT,SDept VARCHAR(20) DEFAULT 计算机 );CRE…

zabbix监控扩展

目录 一、zabbix自动发现与自动注册 &#xff08;一&#xff09;理论定义 1.自动发现 2.自动注册 &#xff08;二&#xff09;实操部署 1.自动发现 &#xff08;1&#xff09;新增一台客户端命名为zbx-agent02 ① 配置时间同步 ② 在服务端和客户端上配置 hosts 解析 …

枚举类型缝缝补补

✅作者简介&#xff1a;大家好&#xff0c;我是橘橙黄又青&#xff0c;一个想要与大家共同进步的男人&#x1f609;&#x1f609; &#x1f34e;个人主页&#xff1a;橘橙黄又青-CSDN博客 1.关键字enum的定义 enum是C语言中的一个关键字&#xff0c;enum叫枚举数据类型&#…

天津想转行学python培训班靠谱吗?

现在的职业如此繁多&#xff0c;很多人把高薪当成衡量工作好坏的重要标准&#xff0c;因此IT行业以超出其他行业几倍薪资水平成为不错的选择&#xff0c;而Python又以其简单易学好上手成为大家所青睐的学习目标。 Python发展前景如何 Python语言就业发展方向广泛&#xff1a;…