前情提要

只要写过 c/c++ 的项目的童鞋应该对对象生命周期的问题记忆犹新。怕有人还不理解这个问题,笔者先介绍下什么是生命周期的问题?

一个 struct 结构体生命周期分为三个步骤:

  1. 出生:malloc 分配结构体内存,并且初始化;

  2. 使用:这个就是对内存的常规使用了;

  3. 销毁: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 等一系列情况。

怎么才能解决生命周期的问题?

生命周期的问题是每个程序猿都可能遇到的,只要程序中涉及到资源的创建、使用、释放,这三个过程,那么生命周期的问题就是你必经之路,这是一个通用的问题

上面我们提到生命周期问题的两个维度,那么解决也是这两个维度的针对性解决。遵守两个原则

  1. 对象在有人使用的时候不能释放;

  2. 对象不仅要释放自身内存还要释放管理的资源

思考下:你在编程的时候,怎么处理的?

下面我从 c 这种底层语言,还有 Go 这种自带 GC 的语言对比出发,来体验下不同语言下的生命周期的问题怎么解决。

c 编程的惯例

c 怎么才能保证内存的安全,资源的安全释放呢?

以下面的场景举例:

  1. 现在有一个 fd_t 的 list 链表,为了保护这个链表,用一个互斥锁来保护 ;

  2. 创建 fd_t 的时候,需要添加进 list(添加会加互斥锁);

  3. 正常使用的时候,会遍历 list ,取合适的元素使用;

  4. fd_t 销毁的时候,会从全局链表中摘除;

首先,list 链表的并发安全可以用互斥锁来解决,但是怎么保证你取出来元素之后,还在处理的时候,一直是安全的呢(不被释放)?

你可能会自然想到一个思路:全程在锁内不就可以了。

确实如此,对象的创建,使用,删除,全程用锁保护,确实可以解决这个问题。但是锁度变得非常大,在现实生产环境的编程中,很少见。

其实,解决资源释放的场景,有一个通用的技术:引用计数。 wiki 上的解释:

引用计数是计算机编程语言中的一种内存管理技术,是指将资源(可以是对象、内存或磁盘空间等等)的被引用次数保存起来,当被引用次数变为零时就将其释放的过程。使用引用计数技术可以实现自动资源管理的目的。

引用计数是一种通用的资源管理技术,简述引用计数用法:

  1. 资源初始化的时候,计数为 1 ;

  2. 就是在资源获取的时候,对资源计数加 1 ;

  3. 资源使用完成的时候,对资源计数减 1 ;

  4. 计数为 0 的时候,走释放流程 ;

这样,只需要用户对资源的使用上遵守一个规则:获取的时候,计数加 1,处理完了,计数减 1 ,就能保证不会有问题。因为在你使用期间,不管别人怎么减,都不可能会到 0 。

思考下:引用计数有什么缺点呢?

  1. 第一个问题,非常容易出错,加减引用一定要配对,一旦有些地方多加了,或者多减了,就会引发资源问题。要么就是泄漏,要么就是使用释放了的资源;

  2. 第二个问题,在于流程上变复杂了,因为计数为 0 的地方点变得不确定了。可能会出现在读元素的流程上,走释放流程;

以上两点,其实对程序猿的能力、细致提出了很高的要求。

Go 就厉害了

引用计数是通用的技术,适用于所有的语言。笔者在写 Go 的时候就用引用计数来解决过资源释放的问题。

但后来发现,Go 语言其实可以把代码写的更简单,Go 的创建则从两个的角度解决了对象生命周期的问题:

第一,根本不让用户释放内存;

Go 的内存,程序猿只能触发分配,无法主动释放。释放内存的动作完全交给了后台 GC 流程。这就很好的解决了第一个问题,由于不让粗心的程序猿参与到资源的管理中,内存资源的管理完全由框架管理(框架强,则我强,嘿嘿),根本就不用担心会被程序猿用到生命终结的内存块。

第二,提供析构回调函数机制;

上面说了,GC 能够保证内存结构体本身的安全性,但是一些句柄资源的释放却无法通过上面保证,怎么办?

Go 提供了一个非常好的办法:设置析构函数。使用 runtime.SetFinalizer 来设置,将一个对象的地址和一个析构函数绑定起来,并且注册到框架里。当对象被 GC 的时候,析构函数将会被框架调用,程序猿则可以把资源释放的逻辑写到析构函数中,这样就配合上了呀,就能保证:在对象永远不能被程序猿摸到的前提下,调用了析构函数,从而完成资源释放

1   生命结束的回调

函数原型:

func SetFinalizer(obj interface{}, finalizer interface{})

参数解析:

  • 参数 obj 必须是指针类型

  • 参数 finalizer 是一个函数,参数为 obj 的类型,无返回值

函数调用 runtime.SetFinalizerobjfinalizer 关联起来。对象 obj 被 Gc 的时候,Go 会自动调用 finalizer 函数,并且 obj 作为参数传入。

就这样,关于生命周期的问题,在 Go 里面就非常优雅的解决了,对象内存释放交给了 Gc,资源释放交给了 finalizer ,程序猿又可以躺好了。

扩展思考

c++ 和 Python 这两种语言又是怎么解决内存的生命周期,还有资源的安全释放呢?

提示:这两种语言都有构造函数和析构函数,但各有不同。这个问题留给读者朋友思考。

  • c++ 有构造函数和析构函数,也很方便,但是 c++ 的类却是非常复杂的。且 c++ 是没有 GC 的,内存释放的动作还是交给了程序猿,所以在 c++ 编程中,引用计数技术还是大量使用的;

  • python 是一个自带 GC ,并且提供构造和析构函数的。所以 python 的使用,程序猿完全不管内存释放,资源释放则只需要定义在类的析构函数里即可;

总结

  1. 生命周期的问题是老大难的问题,分为结构内存的安全释放,内部管理资源的安全释放两个维度;

  2. c/c++ 大量采用引用计数技术来完成对资源的安全释放;

  3. 引用计数的难点在于加减计数的配套使用,并且释放的现场不确定

  4. Go 通过内存自动 Gc ,且提供析构函数绑定到对象地址的方法,从而完美解决了对象生命周期的问题;

  5. runtime.SetFinalizer 替代引用计数的使用,太香了;

后记

你 open  一个文件得到句柄 fd,紧接 unlink  这个文件,此时,还可用 fd 来正常读写文件。直到 close  这个文件的时候,这个文件才会永远的消失。你能猜到其中原理吗?

~完~

往期推荐

往期推荐

自制文件系统 —— 03 Go实战:hello world 的文件系统

假如 Go 能说话,听听 GMP 的心声

存储基础 — 文件描述符 fd 究竟是什么?

深度剖析 Linux cp 的秘密

坚持思考,方向比努力更重要。关注我:奇伢云存储

编程思考:对象生命周期的问题相关推荐

  1. ASP.NET Core Web API下事件驱动型架构的实现(二):事件处理器中对象生命周期的管理

    在ASP.NET Core Web API下事件驱动型架构的实现(一):一个简单的实现中,我介绍了事件驱动型架构的一种简单的实现,并演示了一个完整的事件派发.订阅和处理的流程.这种实现太简单了,百十行 ...

  2. 容器,对象生命周期管理的基石

    2019独角兽企业重金招聘Python工程师标准>>> 郑重申明:包括本文在内的很多技术文章,大多出自山外高人,而非Fans. Fans暂时没有能力写作优秀的技术文章,Fans只是转 ...

  3. [js] 说说你对js对象生命周期的理解

    [js] 说说你对js对象生命周期的理解 一切皆对象 咱们经常听到JS中"一切皆对象"?有没有问想过这是什么意思?其它语言也有"一切皆对象"之说,如Python ...

  4. MinIO对象生命周期管理解析

    目录 前言 对象过期 远程存储层(Tiers)常用分层场景 跨存储介质 跨云类型 公有云 文件迁移实例 Tiers配置 生成周期管理配置 原始桶的tiertest前缀的文件目录 远程存储层目录结构 原 ...

  5. Java 对象生命周期

    Java 对象生命周期 一直对Java对象的实例化.对象.对象的引用.堆 栈存放的内容迷惑不解.看了 Java编程思想,理解似乎又深了一层. 对象和对象的引用 Java 编程思想中,把对象的引用比喻成 ...

  6. 【转】【iOS知识学习】_视图控制对象生命周期-init、viewDidLoad、viewWillAppear、viewDidAppear、viewWillDisappear等的区别及用途...

    原文网址:http://blog.csdn.net/weasleyqi/article/details/8090373 iOS视图控制对象生命周期-init.viewDidLoad.viewWillA ...

  7. 《Imperfect C++中文版》——2.1 对象生命周期

    本节书摘来自异步社区出版社<Imperfect C++中文版>一书中的第2章,第2.1节,作者: [美]Matthew Wilson,更多章节内容可以访问云栖社区"异步社区&qu ...

  8. iOS视图控制对象生命周期-init、viewDidLoad、viewWillAppear、v...

    2019独角兽企业重金招聘Python工程师标准>>> iOS视图控制对象生命周期-init.viewDidLoad.viewWillAppear.viewDidAppear.vie ...

  9. Spring.NET学习笔记(5)-对象生命周期和创建者对象

    一.对象生命周期 说白了就是一init初始化方法和Dispose方法 两种实现方式 1.实现接口方法(造成耦合,放弃),IInitializingObject / init-method和IDispo ...

最新文章

  1. 徐州联赛选拔赛 - 判断能否构成树
  2. 30-seconds-code——math
  3. jenkins使用docker部署web应用
  4. java web modules_使用Java web工程建立Maven Web Module工程
  5. 波的折射现象,你都了解吗?
  6. JSK-127 进制转换【进制】
  7. 面试字节跳动社招,我工资涨了60%,附带面经
  8. 8)Thymeleaf 基本对象表达式
  9. 【PDF下载】大数据峰会之地产大数据趋势与应用实践
  10. USG6310恢复管理员密码
  11. 【deepin 20.1】终端的介绍
  12. win2000修改主机名称
  13. 基于 SpringBoot+Vue 的开源数据可视化分析工具
  14. Python | 打印三角形图案(educoder)
  15. excel里面怎么排名次
  16. fmdb(FMDatabase) 数据库总结
  17. 蚂蚁开放平台开发第三方授权登陆(三):Android端
  18. C语言【海伦公式 求三角形面积】
  19. 标准生物钟作息时间表
  20. 深度学习quora问答

热门文章

  1. Hadoop的搭建,VmwareWorkstation 16pro + Ubuntu18.04.1
  2. win32 输出文字时清除之前的_努力学习没效果?3个步骤,强化沟通输出,实现飞跃式成长...
  3. pytorch教程龙曲良06-10
  4. BAT Window批量重命名
  5. 关于使用fastjson统一序列化响应格式。
  6. 复现强网杯python is the best language 2
  7. 回到网易后开源APM技术选型与实战
  8. PV 和 UV IP
  9. PHP设计模式——享元模式
  10. iptables tcp wrappers