python GC RAII GIL

  • RAII
  • python内存管理
  • __del__
  • GIL

RAII

RAII 是一个资源管理工具,约束在代码执行走出特定作用域之后,不管是正常流程,还是异常流程,都不会漏掉资源的释放,可以极大简化代码编写(不用每个分支都增加资源释放逻辑)和资源管理。多数情况下,都应该尽早释放资源,而不应该依赖垃圾收集不可控的生命周期,比如文件描述符、数据库连接。RAII可以严格绑定资源的有效期与变量的生命周期。

  • python 在 RAII 对应的工具是 with 语句,和 try.. catch 语句中的 finally 子句。
  • go 里面可以用 defer

python with 语句实现RAII

  • Python 的 with 語法使用教學:Context Manager 資源管理器(简单直接,清晰易懂)
  • 浅谈 Python 的 with 语句

  • PEP 343 -- The "with" Statement(重点看Specification: The 'with' Statement 和 Examples)

python内存管理

python内存管理采用的是引用计数机制为主,大部分的内存资源可以做到立即释放。针对存在循环引用的情况,需要通过GC来做内存回收(引用计数不算在GC里面)。python的GC很简单,主要是标记-清除和分代收集,具体标记-清除和分代收集细节见读书笔记:内存管理与GC那点事儿。

The existing reference-counting scheme destroys objects as soon as they become unreachable, except for objects in reference cycles. Those are collected later by Python's cycle collector. Some CPython programs depend on this, e.g. to close files promptly, so it would be nice to keep this feature. (https://wiki.python.org/moin/GlobalInterpreterLock)

看起来python内存管理实现很高效,引用计数能够管理大部分的内存释放,GC的策略和实现也很简单。只有存在循环引用的情况才需要GC的介入,GC的工作量相对比较少。然而:

可能很多人会觉得引用计数有啥开销了,反正每次引用计数的更新开销都很小而且还均摊在整个程序的运行过程中,根本无需担心。其实做过编程语言实现的人都多少会知道:朴素的引用计数,除非在剩余内存非常紧迫的条件下,一般来说吞吐量性能(throughput)是低于朴素的tracing GC的。

CPython采用的引用计数是最朴素的实现方式:局部变量、全局变量和对象字段都参与到引用计数中,而且引用计数的更新是在锁下同步的;外加朴素的mark-sweep备份来处理循环引用。然而在朴素引用计数的基础上有许多改进的方案。其实只要能让局部变量不参与到引用计数中,程序的吞吐量性能(throughput)就可以有不少提升——这种做法叫做“延迟引用计数”(deferred reference counting,DRC)。这种做法最初在这篇论文提出:An Efficient, Incremental Garbage Collector,发表于1976年。

标记-删除 vs. 引用计数

乍一看,Python的GC算法貌似远胜于Ruby的:宁舍洁宇而居秽室乎?为什么Ruby宁愿定期强制程序停止运行,也不使用Python的算法呢?

然而,引用计数并不像第一眼看上去那样简单。有许多原因使得不许多语言不像Python这样使用引用计数GC算法:

首先,它不好实现。Python不得不在每个对象内部留一些空间来处理引用数。这样付出了一小点儿空间上的代价。但更糟糕的是,每个简单的操作(像修改变量或引用)都会变成一个更复杂的操作,因为Python需要增加一个计数,减少另一个,还可能释放对象。

第二点,它相对较慢。虽然Python随着程序执行GC很稳健(一把脏碟子放在洗碗盆里就开始洗啦),但这并不一定更快。Python不停地更新着众多引用数值。特别是当你不再使用一个大数据结构的时候,比如一个包含很多元素的列表,Python可能必须一次性释放大量对象。减少引用数就成了一项复杂的递归过程了。

最后,它不是总奏效的。在我的下一篇包含了我这个演讲剩余部分笔记的文章中,我们会看到,引用计数不能处理环形数据结构--也就是含有循环引用的数据结构。

延迟引用计数

只有当赋值器操作堆中的对象时产生的引用计数变更才会立即生效,而操作栈或者寄存器(局部变量)所产生的变更则会延迟执行—>代价:引用计数器不再准确,所以不能立即回收引用计数等于0的对象,转而要引入stop the world来定期修正引用计数。 
当引用变成0之后,需要将其添加到零引用表中(零引用表中的对象都是引用计数为0但可能仍然存活的对象,当赋值器把零引用对象的引用写入到堆中某一对象时,可以将其从零引用表中移除)。 
当堆中内存耗尽时就必须进行垃圾回收:挂起所有赋值器线程并检查零引用表。怎么确定零引用表中的对象是否存活呢?最简单的办法是对根指向的对象进行扫描并增加引用计数。完成这一步后,所有被根引用的对象的引用计数都大于0,那些仍然为0的就是需要回收的垃圾了。

python GC

下面都是很好的资料,强烈推荐

  • python垃圾回收
  • 为什么 Python 工程师很少像 Java 工程师那样讨论垃圾回收?
  • 垃圾回收机制中,引用计数法是如何维护所有对象引用的?
  • Python垃圾回收机制--完美讲解

__del__

如果 __new__ 和 __init__ 是对象的构造器的话,那么 __del__ 就是析构器。它不实现语句 del x (以上代码将不会翻译为 x.__del__(),del x只是将引用计数减去一 )。它定义的是当一个对象进行垃圾回收时候的行为。当一个对象在删除的时需要更多的清洁工作的时候此方法会很有用,比如套接字对象或者是文件对象。注意,如果解释器退出的时候对象还存存在,就不能保证 __del__ 能够被执行,所以 __del__ can’t serve as a replacement for good coding practices ()~~~~~~~

另外,__del__一般不建议使用,主要有以下原因:

  • 如果循环引用检查器打开了(默认是打开的),循环引用的对象是可以检查到的,但是只有没有定义__del__方法的对象会被销毁。即__del__会破坏gc回收循环引用
  • __del__方法能不能被调用,是不确定的,最好不要把资源释放的操作放在这里做。详见python解释器退出时new style class的类属性的__del__方法不会被调用

GIL

In CPython, the global interpreter lock, or GIL, is a mutex that protects access to Python objects, preventing multiple threads from executing Python bytecodes at once. This lock is necessary mainly because CPython's memory management is not thread-safe. (However, since the GIL exists, other features have grown to depend on the guarantees that it enforces.). 详见:https://wiki.python.org/moin/GlobalInterpreterLock

CPython的实现中,GIL难以有效的去除的原因之一就是为了迁就引用计数。当一个Python线程在运行时,它会获取GIL以保证它对对象引用计数的更新是全局同步的。由于引用计数的实现细节被CPython的C+API暴露到了外部的C扩展,要保持这些扩展完全兼容,就得维持或模拟CPython引用计数的实现,这就麻烦了…&oq=CPython的实现中,GIL难以有效的去除的原因之一就是为了迁就引用计数。当一个Python线程在运行时,它会获取GIL以保证它对对象引用计数的更新是全局同步的。由于引用计数的实现细节被CPython的C+API暴露到了外部的C扩展,要保持这些扩展完全兼容,就得维持或模拟CPython引用计数的实现。详见:https://www.zhihu.com/question/38380754

reference

  • Python可以视作同时支持像C++一样的RAII特性,也具有垃圾回收GC的编程语言吗?
  • 慎用python的__del__方法

  • python解释器退出时new style class的类属性的__del__方法不会被调用

  • 垃圾回收(GC)算法介绍(2)——GC引用计数算法

  • 基于引用计数的对象生命期管理

  • GC系列:如何优化引用计数算法(1)

你可能感兴趣的:(python)