Python GIL 机制深度解析:原理、影响与应对策略

拾光
拾光
发布时间:2026-10-13 | 3人浏览过
原创

本文深入解析 Python 全局解释器锁(GIL)的设计初衷、运行机制及其对多线程性能的影响。文章详细阐述了 GIL 如何在单核限制下简化实现,并探讨了解决方案如多进程、ctypes 调用 C 扩展及 IO 密集型场景的优势,帮助开发者理解为何 Python 多线程无法充分利用多核 CPU 以及如何优化程序性能。

1、GIL 简介

GIL 的全称是 Global Interpreter Lock,即全局解释器锁。

1.1 GIL 设计理念与限制

Python 代码的执行由 Python 虚拟机(在 CPython 版本中称为解释器主循环)控制。Python 在设计之初就确立了在解释器的主循环中,同一时刻只有一个线程运行的原则。也就是说,在任意给定时间点,仅有一个线程在解释器中执行。对 Python 虚拟机访问的控制由全局解释锁(GIL)管理,正是这把锁确保了同一时刻只有一个线程能够运行。

当调用外部代码(例如 C 或 C++ 扩展函数)时,GIL 会被锁定,直到该函数执行完毕(由于在此期间没有 Python 字节码运行,因此不会发生线程切换)。

在 Python 中使用的都是操作系统级别的线程,Linux 系统中使用的是 pthread,而 Windows 系统使用的是其原生线程。

从上述概述中可以直观地看出,Python 在同一时刻只能运行一个线程。因此,在多线程环境下,只有当线程获取到全局解释器锁后才能运行。由于全局解释器锁只有一个,即使在多核 CPU 的情况下,Python 也只能发挥单核的功能。

看起来 Python 似乎不够给力,GIL 直接导致 CPython 无法利用物理多核的性能来加速运行。那么为什么会有这样的设计?考虑到 Guido van Rossum 在创造 Python 的上世纪 90 年代,多核 CPU 完全属于不可想象的范畴。如今由于硬件发展速度太快,程序编写就要考虑用尽 CPU 的全部性能,否则就要被淘汰,那么对于 Python 同样也要如此。

上面主要说的是这种设计的劣势,下面再讨论它的优势。

GIL 的设计简化了 CPython 的实现,使得对象模型,包括关键的内建类型如字典,都隐式可以并发访问。锁住全局解释器使得其比较容易的实现对多线程的支持,但也折损了多处理器主机的并行计算能力。

但是不论标准的,还是第三方的扩展模块,都被设计成在进行密集计算任务时释放 GIL。另外还有在做 IO 操作时,GIL 总是被释放。对所有面对内建的操作系统 C 代码的程序来说,GIL 会在这个 IO 调用之前被释放,以允许其它的线程在等待这个 IO 的时候运行。如果是纯计算的程序,没有 IO 操作,解释器会每隔 100 次或每隔一定时间 15ms 去释放 GIL。

这里可以理解为 IO 密集型的 python 比计算密集型的程序更能利用多线程环境带来的便利。

1.2 GIL 对线程执行的影响

多线程环境中,python 虚拟机按照以下方式执行:

  1. 设置 GIL
  2. 切换到一个线程去执行
  3. 运行代码,这里有两种机制:
    1. 指定数量的字节码指令(100 个)
    2. 固定时间 15ms 线程主动让出控制
  4. 把线程设置为睡眠状态
  5. 解锁 GIL
  6. 再次重复以上步骤

上节说到 python 语言和程序一样要考虑用尽 cpu 的性能,下面在讨论 py 的应对方法。

python 的应对方法很简单,在新的 python3 中依然有 GIL,原因大概有下几点:

  • CPython 的 GIL 本意是用来保护所有全局的解释器和环境状态变量的,如果去掉 GIL,就需要更多的更细粒度的锁对解释器的众多全局状态进行保护。或者采用 Lock-Free 算法。无论采用哪一种,要做到多线程安全都会比维系一个 GIL 要难得多。另外改动的还是 CPython 的代码树及其各种第三方扩展也在依赖 GIL。
  • 进一步说,有人做过测试将 GIL 去掉,加入更细粒度的锁。但是实践检测对单线程来说,性能更低。只有利用的物理 cpu 到一定数目后,性能才会比 GIL 版本好。且现在绝大部分的 python 程序都是单线程的。

然后最重要的还在于以下几个方面,简单来说就是 py 不改,一样能实现我们的需求。

  • 自 2.6 引出的多进程标准库 mutilprocessing,让多进程的 python 编写简化到类似多线程的程度,大大减轻 GIL 带来的诸多不利。
  • 利用 ctypes 绕过 GIL:ctypes 可以使 py 直接调用任意的 C 动态库的导出函数。所要做的只是用 ctypes 写 python 代码即可。而且,ctypes 会在调用 C 函数前释放 GIL。

python 中 GIL 使得同一个时刻只有一个线程在一个 cpu 上执行,无法将多个线程映射到多个 cpu 上执行,但 GIL 并不会一直占有,它会在适当的时候释放

import threading
count = 0
def add():
  global count
  for i in range(10**6):
    count += 1

def minus():
  global count
  for i in range(10**6):
    count -= 1

thread1 = threading.Thread(target=add)
thread2 = threading.Thread(target=minus)
thread1.start()
thread2.start()
thread1.join()
thread2.join()
print(count)

分别运行三次的结果:

-59452 60868 -77007

可以看到 count 并不是一个固定值,说明 GIL 会在某个时刻释放,那么 GIL 具体在什么情况下释放呢:

1.执行的字节码行数到达一定阈值

2.通过时间片划分,到达一定时间阈值

3.在遇到 IO 操作时,主动释放

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对本站的支持。

本站声明

本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。

Python 相关产品和应用

全部下载

专题列表