编程思考:对象生命周期的问题
前情提要
只要写过 c/c++ 的项目的童鞋应该对对象生命周期的问题记忆犹新。怕有人还不理解这个问题,笔者先介绍下什么是生命周期的问题?
一个 struct 结构体生命周期分为三个步骤:
出生:
malloc
分配结构体内存,并且初始化;使用:这个就是对内存的常规使用了;
销毁:
free
释放这个内存块;
最典型结构体“生命周期”问题的场景就是:你在使用对象正嗨的时候,被人偷偷把对象销毁了。举个例子:
12:00 时刻:ObjectA 内存 malloc 出来,地址为 0x12345 ;
12:10 时刻:ObjectA 内存地址 0x12345 释放了;
12:12 时刻:程序猿小明拿到了 ObjectA 的地址 0x12345 ,准备大干一场(但他并不知道的是,这个 ObjectA 结构体已经结束了生命,0x12345 地址已经被释放了)于是,踩内存了,全剧终;
生命周期问题的维度
一般来讲,生命周期的问题其实有两个方面:
第一个是结构体本身内存的生命周期 ;
第二个是结构体对象管理的资源( 比如资源句柄 );
1 结构体本身内存
对象结构体本身的生命周期这个很容易理解,这个就是内存的分配和释放。
// 步骤一:分配
obj_addr = malloc(...);
// 步骤二:使用 ...
// 步骤三:释放
free(obj_addr);
如果违反了这条(使用了已经释放的内存块),就会发生踩内存,野指针,未定义地址等一系列奇异事件。如果没正确释放,那么就是内存泄漏。
2 对象管理的资源
这个也很容易理解,比如一个代表 fd_t
的结构体,里面有一个整型字段,代表这个结构体管理的一个文件句柄。当 fd_t
结构体内存被释放的时候,它管理的文件句柄 sys_fd
也是需要 close
的。
struct fd_t {int sys_fd; // 系统句柄(这个需要在合适的时机释放)struct list_head list; // 链表挂接件//...
};
如果违反了这条(使用释放了的资源,比如句柄),那么就会出现 bad descriptor
等一系列情况。
怎么才能解决生命周期的问题?
生命周期的问题是每个程序猿都可能遇到的,只要程序中涉及到资源的创建、使用、释放,这三个过程,那么生命周期的问题就是你必经之路,这是一个通用的问题。
上面我们提到生命周期问题的两个维度,那么解决也是这两个维度的针对性解决。遵守两个原则:
对象在有人使用的时候不能释放;
对象不仅要释放自身内存还要释放管理的资源;
思考下:你在编程的时候,怎么处理的?
下面我从 c 这种底层语言,还有 Go 这种自带 GC 的语言对比出发,来体验下不同语言下的生命周期的问题怎么解决。
c 编程的惯例
c 怎么才能保证内存的安全,资源的安全释放呢?
以下面的场景举例:
现在有一个 fd_t 的 list 链表,为了保护这个链表,用一个互斥锁来保护 ;
创建 fd_t 的时候,需要添加进 list(添加会加互斥锁);
正常使用的时候,会遍历 list ,取合适的元素使用;
fd_t 销毁的时候,会从全局链表中摘除;
首先,list 链表的并发安全可以用互斥锁来解决,但是怎么保证你取出来元素之后,还在处理的时候,一直是安全的呢(不被释放)?
你可能会自然想到一个思路:全程在锁内不就可以了。
确实如此,对象的创建,使用,删除,全程用锁保护,确实可以解决这个问题。但是锁度变得非常大,在现实生产环境的编程中,很少见。
其实,解决资源释放的场景,有一个通用的技术:引用计数。 wiki 上的解释:
引用计数是计算机编程语言中的一种内存管理技术,是指将资源(可以是对象、内存或磁盘空间等等)的被引用次数保存起来,当被引用次数变为零时就将其释放的过程。使用引用计数技术可以实现自动资源管理的目的。
引用计数是一种通用的资源管理技术,简述引用计数用法:
资源初始化的时候,计数为 1 ;
就是在资源获取的时候,对资源计数加 1 ;
资源使用完成的时候,对资源计数减 1 ;
计数为 0 的时候,走释放流程 ;
这样,只需要用户对资源的使用上遵守一个规则:获取的时候,计数加 1,处理完了,计数减 1 ,就能保证不会有问题。因为在你使用期间,不管别人怎么减,都不可能会到 0 。
思考下:引用计数有什么缺点呢?
第一个问题,非常容易出错,加减引用一定要配对,一旦有些地方多加了,或者多减了,就会引发资源问题。要么就是泄漏,要么就是使用释放了的资源;
第二个问题,在于流程上变复杂了,因为计数为 0 的地方点变得不确定了。可能会出现在读元素的流程上,走释放流程;
以上两点,其实对程序猿的能力、细致提出了很高的要求。
Go 就厉害了
引用计数是通用的技术,适用于所有的语言。笔者在写 Go 的时候就用引用计数来解决过资源释放的问题。
但后来发现,Go 语言其实可以把代码写的更简单,Go 的创建则从两个的角度解决了对象生命周期的问题:
第一,根本不让用户释放内存;
Go 的内存,程序猿只能触发分配,无法主动释放。释放内存的动作完全交给了后台 GC 流程。这就很好的解决了第一个问题,由于不让粗心的程序猿参与到资源的管理中,内存资源的管理完全由框架管理(框架强,则我强,嘿嘿),根本就不用担心会被程序猿用到生命终结的内存块。
第二,提供析构回调函数机制;
上面说了,GC 能够保证内存结构体本身的安全性,但是一些句柄资源的释放却无法通过上面保证,怎么办?
Go 提供了一个非常好的办法:设置析构函数。使用 runtime.SetFinalizer
来设置,将一个对象的地址和一个析构函数绑定起来,并且注册到框架里。当对象被 GC 的时候,析构函数将会被框架调用,程序猿则可以把资源释放的逻辑写到析构函数中,这样就配合上了呀,就能保证:在对象永远不能被程序猿摸到的前提下,调用了析构函数,从而完成资源释放。
1 生命结束的回调
函数原型:
func SetFinalizer(obj interface{}, finalizer interface{})
参数解析:
参数
obj
必须是指针类型参数
finalizer
是一个函数,参数为 obj 的类型,无返回值
函数调用 runtime.SetFinalizer
把 obj
和 finalizer
关联起来。对象 obj 被 Gc 的时候,Go 会自动调用 finalizer
函数,并且 obj 作为参数传入。
就这样,关于生命周期的问题,在 Go 里面就非常优雅的解决了,对象内存释放交给了 Gc,资源释放交给了 finalizer
,程序猿又可以躺好了。
扩展思考
c++ 和 Python 这两种语言又是怎么解决内存的生命周期,还有资源的安全释放呢?
提示:这两种语言都有构造函数和析构函数,但各有不同。这个问题留给读者朋友思考。
c++ 有构造函数和析构函数,也很方便,但是 c++ 的类却是非常复杂的。且 c++ 是没有 GC 的,内存释放的动作还是交给了程序猿,所以在 c++ 编程中,引用计数技术还是大量使用的;
python 是一个自带 GC ,并且提供构造和析构函数的。所以 python 的使用,程序猿完全不管内存释放,资源释放则只需要定义在类的析构函数里即可;
总结
生命周期的问题是老大难的问题,分为结构内存的安全释放,内部管理资源的安全释放两个维度;
c/c++ 大量采用引用计数技术来完成对资源的安全释放;
引用计数的难点在于加减计数的配套使用,并且释放的现场不确定;
Go 通过内存自动 Gc ,且提供析构函数绑定到对象地址的方法,从而完美解决了对象生命周期的问题;
用
runtime.SetFinalizer
替代引用计数的使用,太香了;
后记
你 open 一个文件得到句柄 fd,紧接 unlink 这个文件,此时,还可用 fd 来正常读写文件。直到 close 这个文件的时候,这个文件才会永远的消失。你能猜到其中原理吗?
~完~
往期推荐
往期推荐
自制文件系统 —— 03 Go实战:hello world 的文件系统
假如 Go 能说话,听听 GMP 的心声
存储基础 — 文件描述符 fd 究竟是什么?
深度剖析 Linux cp 的秘密
坚持思考,方向比努力更重要。关注我:奇伢云存储
编程思考:对象生命周期的问题相关推荐
- ASP.NET Core Web API下事件驱动型架构的实现(二):事件处理器中对象生命周期的管理
在ASP.NET Core Web API下事件驱动型架构的实现(一):一个简单的实现中,我介绍了事件驱动型架构的一种简单的实现,并演示了一个完整的事件派发.订阅和处理的流程.这种实现太简单了,百十行 ...
- 容器,对象生命周期管理的基石
2019独角兽企业重金招聘Python工程师标准>>> 郑重申明:包括本文在内的很多技术文章,大多出自山外高人,而非Fans. Fans暂时没有能力写作优秀的技术文章,Fans只是转 ...
- [js] 说说你对js对象生命周期的理解
[js] 说说你对js对象生命周期的理解 一切皆对象 咱们经常听到JS中"一切皆对象"?有没有问想过这是什么意思?其它语言也有"一切皆对象"之说,如Python ...
- MinIO对象生命周期管理解析
目录 前言 对象过期 远程存储层(Tiers)常用分层场景 跨存储介质 跨云类型 公有云 文件迁移实例 Tiers配置 生成周期管理配置 原始桶的tiertest前缀的文件目录 远程存储层目录结构 原 ...
- Java 对象生命周期
Java 对象生命周期 一直对Java对象的实例化.对象.对象的引用.堆 栈存放的内容迷惑不解.看了 Java编程思想,理解似乎又深了一层. 对象和对象的引用 Java 编程思想中,把对象的引用比喻成 ...
- 【转】【iOS知识学习】_视图控制对象生命周期-init、viewDidLoad、viewWillAppear、viewDidAppear、viewWillDisappear等的区别及用途...
原文网址:http://blog.csdn.net/weasleyqi/article/details/8090373 iOS视图控制对象生命周期-init.viewDidLoad.viewWillA ...
- 《Imperfect C++中文版》——2.1 对象生命周期
本节书摘来自异步社区出版社<Imperfect C++中文版>一书中的第2章,第2.1节,作者: [美]Matthew Wilson,更多章节内容可以访问云栖社区"异步社区&qu ...
- iOS视图控制对象生命周期-init、viewDidLoad、viewWillAppear、v...
2019独角兽企业重金招聘Python工程师标准>>> iOS视图控制对象生命周期-init.viewDidLoad.viewWillAppear.viewDidAppear.vie ...
- Spring.NET学习笔记(5)-对象生命周期和创建者对象
一.对象生命周期 说白了就是一init初始化方法和Dispose方法 两种实现方式 1.实现接口方法(造成耦合,放弃),IInitializingObject / init-method和IDispo ...
最新文章
- 徐州联赛选拔赛 - 判断能否构成树
- 30-seconds-code——math
- jenkins使用docker部署web应用
- java web modules_使用Java web工程建立Maven Web Module工程
- 波的折射现象,你都了解吗?
- JSK-127 进制转换【进制】
- 面试字节跳动社招,我工资涨了60%,附带面经
- 8)Thymeleaf 基本对象表达式
- 【PDF下载】大数据峰会之地产大数据趋势与应用实践
- USG6310恢复管理员密码
- 【deepin 20.1】终端的介绍
- win2000修改主机名称
- 基于 SpringBoot+Vue 的开源数据可视化分析工具
- Python | 打印三角形图案(educoder)
- excel里面怎么排名次
- fmdb(FMDatabase) 数据库总结
- 蚂蚁开放平台开发第三方授权登陆(三):Android端
- C语言【海伦公式 求三角形面积】
- 标准生物钟作息时间表
- 深度学习quora问答
热门文章
- Hadoop的搭建,VmwareWorkstation 16pro + Ubuntu18.04.1
- win32 输出文字时清除之前的_努力学习没效果?3个步骤,强化沟通输出,实现飞跃式成长...
- pytorch教程龙曲良06-10
- BAT Window批量重命名
- 关于使用fastjson统一序列化响应格式。
- 复现强网杯python is the best language 2
- 回到网易后开源APM技术选型与实战
- PV 和 UV IP
- PHP设计模式——享元模式
- iptables tcp wrappers