什么是 Python 全局解释器锁(GIL)?

更新于 2026-01-11

Abhinav Ajitsaria

Python 的全局解释器锁(Global Interpreter Lock,简称 GIL)用简单的话来说,就是一个互斥锁(mutex),它确保在任意时刻只有一个线程能够控制 Python 解释器。

这意味着,在任何时间点,只能有一个线程处于执行状态。对于运行单线程程序的开发者而言,GIL 的影响几乎不可见;但对于 CPU 密集型或多线程代码来说,GIL 可能成为一个性能瓶颈。

由于 GIL 即使在拥有多个 CPU 核心的多线程架构中也只允许一个线程执行,因此它在 Python 社区中获得了“臭名昭著”的声誉。

在本文中,你将了解 GIL 如何影响你的 Python 程序性能,以及如何减轻它可能对你的代码造成的影响。


GIL 为 Python 解决了什么问题?

Python 使用引用计数(reference counting)进行内存管理。这意味着在 Python 中创建的对象都有一个引用计数变量,用于跟踪指向该对象的引用数量。当这个计数变为零时,对象所占用的内存就会被释放。

让我们看一个简短的代码示例,演示引用计数的工作方式:

>>> import sys
>>> a = []
>>> b = a
>>> sys.getrefcount(a)
3

在上面的例子中,空列表对象 [] 的引用计数为 3。该列表对象被变量 ab 以及传给 sys.getrefcount() 的参数所引用。

回到 GIL:

问题是,这个引用计数变量需要防止竞态条件(race condition)——即两个线程同时增加或减少其值。如果发生这种情况,可能会导致内存泄漏(永远无法释放)或者更糟的情况:在仍有引用存在的情况下错误地释放内存。这会导致程序崩溃或其他“奇怪”的 bug。

为了保护引用计数变量,一种方法是在所有跨线程共享的数据结构上加锁,以避免不一致的修改。

但为每个对象或对象组都添加锁会带来另一个问题:死锁(deadlock)(只有存在多个锁时才可能发生)。此外,频繁地获取和释放锁也会显著降低性能。

GIL 的设计思路是:在解释器本身上设置一个单一的锁,规定任何 Python 字节码的执行都必须先获得这个解释器锁。这样既避免了死锁(因为只有一个锁),又不会引入太多性能开销。但它实际上使得任何 CPU 密集型的 Python 程序变成了单线程。

虽然其他语言(如 Ruby)的解释器也使用了类似的 GIL,但这并非解决线程安全内存管理的唯一方案。一些语言通过采用垃圾回收(garbage collection)等非引用计数的机制,完全避开了对 GIL 的需求。

另一方面,这些语言往往需要通过其他性能增强特性(例如 JIT 编译器)来弥补因缺少 GIL 而损失的单线程性能优势。


为什么选择 GIL 作为解决方案?

那么,为什么 Python 会选择这样一个看似阻碍并发的方案?这是 Python 开发者的一个糟糕决定吗?

正如 Larry Hastings 所言,GIL 的设计决策恰恰是让 Python 如今如此流行的原因之一。

Python 诞生于操作系统尚无线程概念的时代。它被设计为易于使用,以加快开发速度,因此吸引了越来越多的开发者。

当时,大量 C 语言库的扩展被编写出来,以便在 Python 中使用这些库的功能。为了防止数据不一致,这些 C 扩展需要线程安全的内存管理机制,而 GIL 正好提供了这一点。

GIL 实现简单,很容易集成到 Python 中。它还能提升单线程程序的性能,因为只需管理一个锁。

那些原本不是线程安全的 C 库也因此更容易集成。这些 C 扩展成为 Python 被不同社区广泛采用的重要原因之一。

由此可见,GIL 是 CPython 开发者在 Python 早期面对一个棘手问题时所采取的一种务实解决方案。


GIL 对多线程 Python 程序的影响

当你观察一个典型的 Python 程序(或任何计算机程序)时,可以将其分为两类:CPU 密集型(CPU-bound)和 I/O 密集型(I/O-bound)。

  • CPU 密集型程序:这类程序会将 CPU 推至极限,例如进行数学计算(如矩阵乘法、搜索、图像处理等)。
  • I/O 密集型程序:这类程序花费大量时间等待输入/输出操作,例如来自用户、文件、数据库或网络的数据。由于数据源本身可能需要时间处理(如用户思考输入内容,或数据库执行查询),I/O 操作常常需要等待。

让我们看一个简单的 CPU 密集型程序,它执行一个倒计时:

single_threaded.py

import time
from threading import Thread

COUNT = 50000000

def countdown(n):
    while n > 0:
        n -= 1

start = time.time()
countdown(COUNT)
end = time.time()

print('Time taken in seconds -', end - start)

在我的四核系统上运行结果如下:

$ python single_threaded.py
Time taken in seconds - 6.20024037361145

现在我稍作修改,使用两个线程并行执行相同的倒计时任务:

multi_threaded.py

import time
from threading import Thread

COUNT = 50000000

def countdown(n):
    while n > 0:
        n -= 1

t1 = Thread(target=countdown, args=(COUNT//2,))
t2 = Thread(target=countdown, args=(COUNT//2,))

start = time.time()
t1.start()
t2.start()
t1.join()
t2.join()
end = time.time()

print('Time taken in seconds -', end - start)

再次运行:

$ python multi_threaded.py
Time taken in seconds - 6.924342632293701

可以看到,两个版本的耗时几乎相同。在多线程版本中,GIL 阻止了 CPU 密集型线程真正并行执行。

对于 I/O 密集型的多线程程序,GIL 的影响不大,因为在线程等待 I/O 时会释放 GIL,从而让其他线程有机会运行。

然而,如果一个程序的所有线程都是 CPU 密集型的(例如使用多个线程分别处理图像的不同部分),那么不仅会因 GIL 而退化为单线程执行,甚至总执行时间还会比纯单线程版本更长——正如上面的例子所示。这种额外开销来源于 GIL 的获取与释放。


为什么 GIL 还没有被移除?

Python 开发者经常收到关于 GIL 的抱怨,但像 Python 这样广泛使用的语言,不能轻易做出移除 GIL 这样重大的变更,否则会造成严重的向后兼容性问题。

当然,GIL 是可以被移除的,过去已有多个开发者和研究人员尝试过,但这些尝试都破坏了大量依赖 GIL 提供线程安全机制的现有 C 扩展。

诚然,还有其他方法可以解决 GIL 所应对的问题,但有些方案会降低单线程或 I/O 密集型多线程程序的性能,有些则实现起来过于复杂。毕竟,没人希望自己的现有 Python 程序在新版本发布后反而变慢了,对吧?

Python 创始人兼终身仁慈独裁者(BDFL)Guido van Rossum 在 2007 年 9 月的一篇文章《It isn’t Easy to remove the GIL》中回应了社区:

“我只会在满足以下条件的情况下,接受将补丁合并到 Py3k(即 Python 3):单线程程序(以及多线程但 I/O 密集型程序)”

截至目前,还没有任何移除 GIL 的尝试能满足这一条件。


为什么在 Python 3 中没有移除 GIL?

Python 3 本有机会从头开始重构许多特性,并在此过程中破坏了一些现有的 C 扩展,导致这些扩展需要更新和移植才能兼容 Python 3。这也是 Python 3 早期版本被社区缓慢采纳的原因之一。

但为什么没有趁此机会一并移除 GIL 呢?

因为移除 GIL 会导致 Python 3 的单线程性能比 Python 2 更差,你可以想象这会引发怎样的后果。GIL 带来的单线程性能优势是不可否认的。因此,Python 3 仍然保留了 GIL。

不过,Python 3 对 GIL 做了一项重要改进:

我们讨论了 GIL 对“纯 CPU 密集型”和“纯 I/O 密集型”多线程程序的影响,但现实中很多程序是混合型的——既有 CPU 密集型线程,也有 I/O 密集型线程。

在旧版 Python 中,GIL 会导致 I/O 密集型线程“饥饿”——即无法从 CPU 密集型线程手中抢到 GIL。

这是因为 Python 内部有一个机制:线程在连续执行一定数量的字节码指令后必须释放 GIL。如果此时没有其他线程请求 GIL,原线程可以立即重新获取它。

>>> import sys
>>> # 默认间隔为 100 条指令:
>>> sys.getcheckinterval()
100

问题在于,大多数情况下 CPU 密集型线程会在其他线程之前重新抢回 GIL。David Beazley 对此进行了深入研究,相关可视化可在此处查看。

这个问题在 2009 年的 Python 3.2 中由 Antoine Pitrou 修复。他引入了一种新机制:当检测到有其他线程多次请求 GIL 但未成功时,会强制当前线程放弃 GIL,让其他线程有机会运行。


如何应对 Python 的 GIL?

如果你的程序确实受到 GIL 的限制,可以尝试以下几种方法:

1. 多进程 vs 多线程

最常用的方法是采用多进程(multiprocessing)而非多线程。每个 Python 进程拥有独立的 Python 解释器和内存空间,因此 GIL 不再是问题。

Python 的 multiprocessing 模块可以轻松创建进程:

multiprocess.py

from multiprocessing import Pool
import time

COUNT = 50000000
def countdown(n):
    while n > 0:
        n -= 1

if __name__ == '__main__':
    pool = Pool(processes=2)
    start = time.time()
    r1 = pool.apply_async(countdown, [COUNT//2])
    r2 = pool.apply_async(countdown, [COUNT//2])
    pool.close()
    pool.join()
    end = time.time()
    print('Time taken in seconds -', end - start)

在我的系统上运行结果如下:

$ python multiprocess.py
Time taken in seconds - 4.060242414474487

相比多线程版本,性能有了明显提升!

不过要注意,时间并未减半,因为进程管理本身也有开销。多进程比多线程更“重”,在高并发场景下可能成为扩展瓶颈。

2. 使用其他 Python 解释器

Python 有多种解释器实现。最流行的包括:

  • CPython(C 语言编写,默认实现,有 GIL
  • Jython(Java 编写)
  • IronPython(C# 编写)
  • PyPy(Python 编写,带 JIT)

其中,只有 CPython 有 GIL。如果你的程序及其依赖库支持其他解释器,不妨尝试它们。

3. 耐心等待

尽管许多 Python 用户受益于 GIL 带来的单线程性能优势,但多线程程序员也不必灰心——Python 社区中最聪明的一批人正在努力从 CPython 中彻底移除 GIL。其中一个著名尝试叫做 Gilectomy


总结

Python 的 GIL 常被视为一个神秘而复杂的话题。但请记住,作为 Python 开发者,你通常只有在以下两种情况下才会受到它的影响:

  • 编写 C 扩展
  • 在程序中使用 CPU 密集型的多线程

如果是这样,本文已为你提供了理解 GIL 本质及其应对策略所需的一切知识。