如果 Python 4.0 摆脱了 GIL…( 二 )


 

  • CPython
 
简单来讲 , CPython 就是我们用的 Python 。只是为了更容易地与“Python这门语言”进行区分 , 我们一般把运行 Python 解释器的这个引擎叫做 CPython(我一开始就把 CPython 跟 Cython 项目搞混了 , 但其实 Cython 和我们本文说的就不是一回事了 , 它只是个把 Python 变成 C 的工具) 。那除了用 C 写的 CPython , 其实还有用 JAVA 和 C# 分别写的 Jython 和 IronPython 。值得注意的是 , 后两者并没有 GIL , 因此 GIL 并不是 Python 这个语言的特性/问题 , 而是 CPython 实现中包含的 , 因此下文与 GIL 相关的都会用 CPython 这个名字进行阐述 。
 
  • Reference Counting
 
当你有了变量的时候 , CPython 就已经开始计数(counting)了 , 而当这个变量出现在任一列表(list)或者字典(dict)或者函数(function)内的时候 , 计数都会增加 。当使用变量的函数执行完毕 , 或这个变量被 pop 出了某一个列表的时候 , CPython就会把这个变量的计数对应减少 。而当某个变量计数为零的时候 , 这个变量所在的内存就可以被释放掉了 , 可以看CPython 源码这里(https://github.com/python/cpython/blob/main/Include/object.h#L520)就是这么写的 。
具体计数方式也可见下面的代码例子 , 我个人觉得看代码更容易理解:
如果 Python 4.0 摆脱了 GIL…

文章插图
 
而这个 Reference Counting 有意思的地方就在于:程序释放变量对应的内存空间无需等待GC工作时再进行操作了!因为只要计数为零 , 就满足了条件 。那为什么 CPython 还是需要 GC 呢?我这里一下子也没想通 , 查阅了一下资料发现原因其实很简单 , 因为如果有几个变量没其他地方用到了 , 但是它们互相之间是有 reference 的 , 那这个时候仅靠 Reference Counting 去释放内存自然就会发生内存泄漏 。
 
  • Atomicity
 
继续用上面“纸上写数字”的例子 , 当你在写数字“17”时 , 你要先写个“1” , 再写个“7”吧 。可能在你写“7”之前 , 会有人把你的笔抢走 , 这个OK 。但你起码不能在写“1”写到一半的时候 , 允许别人打断你 。换句话说 , 你要有一个最“原子”的行为 , 这个行为无法进一步再拆分 , 你就Atomic了 。
这个概念 , 有一些数据库知识基础的话 , 应该不需要解释 , 跟大部分数据库所保证的 Atomic 是不同场景下的同一个意思 。
 
  • Concurrent Collection Protections
 
列表(list)或者字典(dict)这类对象 , 我们都可以称之为 Collection , 这些对象往往在类似 Python 这种语言内都有各自独特的内存结构 , 有的结构倾向于计算速度 , 而有的结果是出于内存占用考虑进行了优化 。但无论哪种结构 , 在出现并发的的情况下 , 这些 Collection 都存在线程安全问题 , 因此处理时底层往往有一定的并发锁逻辑进行保护 , 这个相信不难理解 。
改写历史的 nogil 项目的技术细节
Sam 大神的新项目 nogil 之所以获得了如此大的关注度 , 也首次引起 CPython 核心团队好评的原因不仅是它成功移除掉了 GIL (而非类似 per-interpreter GIL 那种半移不移的设计) , 同时也克服了绝大多数前人未能解决的问题 , 而且最终性能分数惊人 。通读了 Sam 的原 paper 后 , 我又翻阅了几篇大神们对该工作的讨论和文章 , 感觉这个项目成功的核心倒不是设计上有多么巧妙(当然人家非常非常非常巧妙) , 难得的是 nogil 把 Python 目前版本里几个 “浪费” 掉的地方拎出来逐一进行了深度优化 , 只不过“深”到已经把 Python 的内存分配器 PyMalloc 都直接换掉了 。正像 Larry Hastings 所说 , 难的不是移除掉 GIL , 难的是移除掉了 GIL 还能保证以前的东西就像没移除掉一样好用... 那 Sam 是怎么做到呢?这里讨论下我觉得比较有意思的几点:


推荐阅读