GIL 解释及探究

什么是 GIL ?

GIL,全称 Global Interpreter Lock, 即全局解释器锁, 它的官方解释如下:

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.)

在 CPython 解释器中,全局解释器锁 GIL 是在于执行 Python 字节码时,为了保护访问 Python 对象而阻止多个线程执行的一把互斥锁。
这把锁的存在主要是因为 CPython 解释器的内存管理不是线程安全的。然而直到今天 GIL 依旧存在,现在很多功能已经习惯于依赖它作为执行的保证。

  • GIL 是存在于 CPython 解释器中的,属于解释器层级,而并非属于 Python 的语言特性。也就是说,如果你自己有能力
    实现一个 Python 解释器,完全可以不用 GIL。
  • GIL 是为了让解释器在执行 Python 代码时,同一时刻只有一个线程在运行,以此保证内存管理是安全
  • 历史原因,现在很多 Python 项目已经习惯于依赖 GIL (开发者认为 Python 就是线程安全的,写代码时对共享资源的访问不会加锁)

常见的 Python 解释器还有:

  • CPython
  • IPython
  • PyPy: 目标是加快执行速度,采用 JIT 技术,对 Python 代码进行动态编译,可以显著提高代码的执行速度。
  • Jpython: 运行在 Java 平台的 Python 解释器,可以把 Python 编译为 Java 字节码
  • IronPython: .Net 平台

GIL 带来的问题

一个 Python 线程想要执行一段代码,必须先拿到 GIL 锁后才被允许执行,也就是说,即使我们使用了多线程,但同一时刻却只有一个线程在执行。

GIL 原理

其实,由于 Python 的线程就是 C 语言的 pthread, 它是通过操作系统调度算法调度执行的。

python3.x 进行了优化,基于固定时间的调度方式,就是每执行固定时间的字节码,或者遇到系统 IO 时,强制释放
GIL,触发系统的线程调度。

而在python3.x中,GIL不使用 ticks 计数,改为使用计时器(执行时间达到阈值后,当前线程释放GIL),
这样对CPU密集型程序更加友好,但依然没有解决GIL导致的同一时间只能执行一个线程的问题,所以效率依然不尽如人意。

多核多线程比单核多线程更差,原因是单核下多线程,每次释放GIL,唤醒的那个线程都能获取到GIL锁,所以能够无缝执行,
但多核下,CPU0释放GIL后,其他CPU上的线程都会进行竞争,但GIL可能会马上又被CPU0拿到,导致其他几个CPU上被唤醒后的线程会醒着等待到切换时间后又进入待调度状态,这样会造成线程颠簸(thrashing),导致效率更低

而线程在调度时,又依赖系统的 CPU 环境,也就是在单核 CPU 或者多核 CPU 下,多线程在调度切换时的成本是不同的。

如果多线程运行一个 CPU 密集型任务,那么 Python 多线程是无法提高运行效率的。

如果需要运行 IO 密集型任务,Python 多线程是可以提高运行效率的。

解决方案

  • IO 密集型任务场景,可以使用多线程提高运行效率
  • CPU 密集型任务场景,不使用多线程,推荐使用多进程方式部署运行
  • 更换没有 GIL 的 Python 解释器,需要预评估运行结果是否与 CPython 一致
  • 编写 Python 的 C 扩展模块,将 CPU 密集型任务交给 C 模块处理。
-------------THANKS FOR READING-------------