placeholder学习Python GIL:解决的问题&带来的限制

学习Python GIL:解决的问题&带来的限制

由于 GIL 的存在,Python 的多线程并不能有效利用 CPU 多核。但如果只把 GIL 看成性能包袱,就很难理解为什么 CPython 长期保留它,也很难理解为什么“去掉 GIL”会牵动整个解释器与扩展生态。

这篇文章面向已经会写 Python、也知道线程和锁的人。重点介绍:GIL 想解决什么问题,为什么“给每个对象单独加锁”听起来合理却常常更差,以及它为什么让 C 扩展更容易集成。

GIL 是什么

GIL(Global Interpreter Lock) 可以直接翻译成“全局解释器锁”。在任意时刻,只有一个线程可以持有 GIL;持有 GIL 的线程才能执行 Python 字节码[1] & 直接操作解释器状态[2]

         
      

为什么会有 GIL

简化内存管理

CPython 的内存管理长期依赖 引用计数(Reference Counting)。每个对象头里都有一个计数器 ob_refcnt,记录当前有多少地方持有这个对象。

  • 当有新的变量指向它时:调用 Py_INCREF 令计数 +1
  • 当引用消失(变量销毁、重新赋值等):调用 Py_DECREF 令计数 -1
  • 当计数变为 0 时:对象立即被回收(释放内存)
import sysa = []print(sys.getrefcount(a))  # 通常会比直觉多 1,因为 getrefcount 调用本身也持有引用

这个方案在实现起来相对简单。但标准 CPython 的 Py_INCREFPy_DECREF 只是简单的 C 宏,直接执行 op->ob_refcnt++--,C 语言中的自增、自减均非原子操作,因此在多线程环境下,如果并发修改计数,可能会出现偏差,导致两种后果:

  • 计数偏大:对象无法被释放,造成内存泄漏。
  • 计数偏小,对象可能在仍被使用时提前释放,最终触发悬垂指针甚至段错误。

既然引用计数并发修改不安全,那就给每个对象加一把锁,或者把 Py_INCREF 改成原子的,不就好了吗?

直觉上,这似乎比全局一把大锁更先进,因为锁更细,看起来并发度也更高。但放到 CPython 这种“几乎一切都是对象”的运行时里,事情没有这么简单。

先看一个关键事实:引用计数操作非常频繁,而且临界区极短。

很多普通代码,底层都会触发 INCREFDECREF

  • 变量赋值
  • 函数传参与返回值
  • 容器读写
  • 临时对象创建与销毁
  • 异常传播过程中的对象保存与清理

也就是说,如果采用对象级锁(或更细粒度的锁机制),至少需要考虑下面这些因素。

引用计数更新本质上只是改对象头上的一个整数,但对象级锁获取和释放需要:

  • Atomicity:被锁保护的操作必须是原子操作,即操作不可被线程中断

  • Memory Visibility:要保证一个线程对对象的修改能被其他线程及时看到

  • Cache Coherence:多核 CPU 中,每个核心可能有自己的缓存,对象级锁需要保证不同核心看到的对象状态一致

  • 死锁避免与可重入性

    每个对象带锁会膨胀对象头,影响小整数、字符串、元组等大量小对象。

    结果:

    • 内存占用增加

    • CPU cache 容纳热点对象减少

    • 热点路径性能下降

      Python 操作通常涉及多个对象,例如赋值、list.append(x)

      问题:

      • 多对象锁必须严格顺序,否则易死锁
      • 许多代码路径需重写
      • C 扩展也必须遵守锁协议

      对象级锁不仅慢,还显著增加实现复杂度。

      CPython 选择了直接加一把全局锁,优势有:

      • 引用计数更新天然串行化,不需要对象级锁
      • 很多解释器内部结构可以假设“当前只有一个线程在改”
      • 锁的获取与释放被摊薄到更大的执行片段上,而不是每个对象操作都来一次

      为什么 GIL 让 C 扩展更容易集成

      这部分经常只被一句话带过,但它其实是 GIL 能长期存在的关键原因之一。

      CPython 的 C API 长期建立在一个重要约束上:

      只要扩展代码持有 GIL,它就可以把大部分解释器内部状态当成“当前线程独占”的。

      这意味着一个 C 扩展在操作 PyObject* 时,通常可以直接做这些事,而不必给每个对象再上一层锁:

      • 读写引用计数
      • 访问列表、字典、元组等对象的内部结构
      • 创建新对象、抛出异常、调用 Python API
      • 使用大量历史遗留下来的 C API 宏与快速路径

      这极大降低了扩展作者的心智负担。对很多扩展来说,规则几乎可以简化成一句话:

      碰 Python 对象之前,先确保自己持有 GIL。

      这个默认前提非常重要,因为大量 C 扩展作者并不是并发运行时专家。他们可能擅长数值计算、图像处理、数据库驱动或系统接口,但并不想在每次调用 Python C API 时都重新推导对象生命周期与锁顺序。

      如果没有 GIL,而是改成真正的细粒度并发模型,那么 C 扩展就不能再假设“只要进入 Python API,我就在一个近似单线程的世界里”。

      这会立刻引出一串额外要求:

      • 每次操作 Python 对象前,都要考虑对象是否会被其他线程同时修改
      • borrowed reference(借用引用)、容器迭代器、缓存指针等历史接口会变得更脆弱
      • 扩展内部如果保存了指向 Python 对象内部结构的裸指针,就必须重新审视其生命周期
      • 许多旧扩展需要补锁、改数据结构、调整 API 使用方式

      也就是说,去掉 GIL 不只是“解释器自己改一改”这么简单,而是 整个 CPython C API 合同都需要收紧或重写

      C 扩展的并发性

      GIL 并不是要求 C 扩展全程串行执行。它真正提供的是一个简单边界:

      • 操作 Python 对象时,持有 GIL
      • 执行纯 C 计算或阻塞 I/O 时,可以主动释放 GIL

      这也是为什么很多扩展既能安全集成,又能在关键路径上跑出并行性。典型做法类似这样:

      Py_BEGIN_ALLOW_THREADSdo_blocking_io_or_native_compute();Py_END_ALLOW_THREADS

      这套模型的好处在于默认安全,按需放开。

      扩展作者不必从第一天起就把所有代码写成细粒度线程安全,只需要在“不碰 Python 对象”的阶段显式释放 GIL。

      GIL 给 Python 多线程带来的影响

      CPU 密集型 Python 线程无法真正并行

      如果两个线程都在执行纯 Python 的 CPU 密集型代码,那么它们会轮流持有 GIL,而不是同时在两个核心上并行执行字节码。

      这就是为什么很多人会觉得“Python 开了多线程还是没变快”。

      I/O 密集型场景仍然适合线程

      如果线程大部分时间都在等待网络、磁盘或其他阻塞 I/O,那么线程依然很有价值,因为等待阶段通常会让出 GIL。此时多线程提升的不是单核算力,而是 等待时间的重叠

      原生扩展可以绕开这一限制

      如果耗时部分在 C、C++、Rust 或 Fortran 实现的原生扩展里,而且这些代码在计算阶段释放了 GIL,那么多个线程就可以在底层真正并行。

      通常一些 CPU Bound 的代码会有 C 扩展实现,因此通常不需要关心 GIL 带来的影响

      Warning

      GIL 的存在并不意味着“写 Python 多线程代码时就自动线程安全”。GIL 保护解释器内部状态,并不保护业务变量

      remain_count -= 1 这种 read-modify-write(读、改、写)逻辑,依然可能在多个线程之间交错执行,导致结果错误。

      如何绕过 GIL

      常见思路有三种:

      • 多进程multiprocessing 让每个进程拥有自己的解释器实例与 GIL,适合 CPU 密集型任务。
      • 原生扩展:把热点路径挪到 C、C++、Rust、Cython 或其他可以释放 GIL 的实现里。
      • 换实现或关注 free-threaded 路线:PEP 703 推动的是一条 no-GIL / free-threaded 的 CPython 路线,但这不是“删掉一把锁”那么简单,而是对对象模型、引用计数策略与 C 扩展兼容性的系统性改造。

      总结

      总体来看,全局锁是性能、内存、复杂度的工程折中方案,牺牲 CPU 密集型多线程并行能力,换解释器实现复杂度、单线程路径成本与生态兼容性。

      Read More

      Inside The Python Virtual Machine | 深入理解 Python 虚拟机


      1. Python 字节码(Bytecode)是 Python 源代码(.py)经编译后生成的一种低级、与平台无关的中间代码,存在于 .pyc 文件或内存中。它由 CPython解释器在虚拟机上执行,用于提升程序加载速度。开发者可通过内置的dis模块反编译字节码以深入理解程序运行机制。 ↩︎

      2. Python 解释器(通常是 CPython)在运行过程中维护的所有运行时数据结构和执行环境的集合。包括对象系统(如整数、字符串等)、内存管理(引用计数与 GC)、调用栈与栈帧、线程与解释器状态、字节码执行状态(指令与操作数栈)、全局解释器锁(GIL)、模块与命名空间以及异常处理等。可参考 Python 3.7 源码中的 PyInterpreterState结构体 ↩︎

      学习Python GIL:解决的问题&带来的限制

      https://vluv.space/GIL/

      Author

      GnixAij

      Posted

      2025-06-20

      Updated

      2026-04-19

      License