穿透 Python GIL 的迷雾:它到底锁的是什么?为什么 CPython 仍坚持多线程?
在我这些年教授 Python、参与高性能系统开发的经历中,最常被问到的问题之一就是:
“GIL 到底锁的是什么?是不是锁住了所有线程?”
“既然 GIL 会阻塞多线程并行,那 CPython 为什么还要保留多线程?”
“多线程在 Python 中还有意义吗?”
这些问题背后,是 Python 开发者对性能、并发和底层机制的深深困惑。GIL(Global Interpreter Lock,全局解释器锁)几乎是 Python 世界里最具争议的设计之一,它既是历史遗留,又是现实妥协;既是性能瓶颈,又是稳定基石。
这篇文章,我将带你从 Python 的发展背景讲起,逐步深入 GIL 的本质、触发机制、设计原因与工程实践价值,并结合大量代码示例与真实案例,帮助你真正理解:
✅ GIL 到底锁住了什么
✅ 为什么 CPython 仍然保留多线程
✅ 多线程在 Python 中的正确使用方式
✅ 如何绕过 GIL 获得真正的并行
让我们从头开始。
一、开篇:Python 的发展与 GIL 的诞生
Python 自 1991 年诞生以来,以“简洁、优雅、可读性强”著称。它的设计哲学之一是:
让开发者专注于业务逻辑,而不是内存管理与线程安全。
在 Python 的早期版本中,内存管理依赖引用计数(Reference Counting)。引用计数的更新不是原子操作,因此在多线程环境下会产生竞争条件(Race Condition)。
为了避免复杂的锁设计、提升解释器稳定性、降低开发成本,Python 的作者 Guido van Rossum 做了一个关键决定:
用一个全局锁保护解释器内部状态。
这就是 GIL 的起源。
二、GIL 到底锁的是什么?(核心问题)
很多人以为 GIL 锁住的是:
- 所有线程
- 所有 Python 对象
- 所有 CPU 核心
但这都是误解。
✅ GIL 锁住的不是 Python 对象,而是整个解释器状态(Interpreter State)
更准确地说:
GIL 锁住的是 CPython 的字节码执行器(Bytecode Interpreter)。
也就是说:
- 只有持有 GIL 的线程才能执行 Python 字节码
- 其他线程即使被调度,也无法执行 Python 层面的运算
- GIL 不锁 I/O,不锁 C 扩展,不锁系统调用
✅ GIL 的本质:保护引用计数与对象模型
CPython 的对象模型不是线程安全的:
- 引用计数不是原子操作
- 内存分配器不是线程安全的
- 对象内部状态不是线程安全的
因此:
GIL 是 CPython 为了保证对象模型一致性而加的全局互斥锁。
三、GIL 的工作机制:什么时候会释放?什么时候会阻塞?
GIL 的行为可以总结为:
一个线程持有 GIL → 执行 Python 字节码
其他线程等待 → 无法执行 Python 字节码
但 GIL 并不是一直不放,它会在以下情况释放:
✅ 1. I/O 操作时释放 GIL(最重要)
例如:
import threading
import time
def task():
time.sleep(1) # I/O 操作,释放 GIL
print("done")
for _ in range(10):
threading.Thread(target=task).start()
time.sleep() 会释放 GIL,因此多个线程可以并发执行。
这也是为什么:
✅ Python 多线程适合 I/O 密集型任务
❌ 不适合 CPU 密集型任务
✅ 2. C 扩展主动释放 GIL
例如 NumPy:
import numpy as np
def compute():
a = np.random.rand(10000, 10000)
b = a @ a # C 扩展内部释放 GIL
NumPy 的矩阵运算在 C 层面执行,不受 GIL 限制,可以真正并行。
✅ 3. 线程切换时释放 GIL(每隔一定字节码执行次数)
Python 3.2 之前:
- 每执行 100 条字节码 切换一次线程
Python 3.2 之后:
- 改为基于时间片(默认 5ms)
四、为什么 CPython 仍然保留多线程?
这是很多人最困惑的问题:
“既然 GIL 限制了并行,那 Python 为什么还要多线程?”
答案有三个层面。
✅ 1. 多线程对 I/O 密集型任务非常有效
例如:
- 网络请求
- 文件读写
- 数据库访问
- 爬虫
- 日志处理
这些操作会释放 GIL,因此多线程可以真正并发。
示例:
import requests
import threading
def fetch(url):
print(requests.get(url).status_code)
urls = ["https://example.com"] * 20
for u in urls:
threading.Thread(target=fetch, args=(u,)).start()
多线程可以显著提升吞吐量。
✅ 2. 多线程比多进程更轻量
- 线程共享内存
- 创建成本低
- 上下文切换快
- 适合高并发 I/O
在 Web 服务中,多线程仍然是主流模型之一。
✅ 3. CPython 的生态大量依赖线程
例如:
- logging 模块内部使用线程
- asyncio 的事件循环使用线程池
- concurrent.futures.ThreadPoolExecutor
- 各种 C 扩展内部使用线程
如果移除线程,整个生态将崩溃。
五、GIL 的误区:你以为它限制了所有并行,其实没有
以下情况不受 GIL 限制:
✅ 1. I/O 操作(会释放 GIL)
例如:
- 网络请求
- 文件读写
- sleep
- socket
- subprocess
✅ 2. C 扩展(主动释放 GIL)
例如:
- NumPy
- Pandas
- TensorFlow
- PyTorch
- Pillow
这些库内部使用 C/C++,可以真正多核并行。
✅ 3. 多进程(完全绕过 GIL)
from multiprocessing import Pool
def f(x):
return x * x
with Pool(4) as p:
print(p.map(f, range(10)))
每个进程有自己的 GIL,因此可以真正并行。
六、实战:如何绕过 GIL 获得真正的并行?
下面给出工程实践中最常用的三种方式。
✅ 方式 1:使用多进程(multiprocessing)
适合 CPU 密集型任务:
from multiprocessing import Pool
def cpu_task(n):
return sum(range(n))
with Pool() as p:
print(p.map(cpu_task, [10_000_000] * 4))
✅ 方式 2:使用 C 扩展(NumPy、Cython、PyPy)
例如 NumPy:
import numpy as np
a = np.random.rand(20000, 20000)
b = a @ a # 真正并行
✅ 方式 3:使用 asyncio(适合 I/O)
import asyncio
import aiohttp
async def fetch(session, url):
async with session.get(url) as resp:
return await resp.text()
async def main():
async with aiohttp.ClientSession() as session:
tasks = [fetch(session, "https://example.com") for _ in range(20)]
await asyncio.gather(*tasks)
asyncio.run(main())
七、GIL 的未来:会被移除吗?
Python 社区一直在探索移除 GIL 的可能性。
✅ PEP 703:可选无 GIL 的 CPython(Huge Progress)
2023 年,Sam Gross 提出:
让 CPython 支持无 GIL 模式(可选)
目前已经合并到 Python 3.13 的实验性分支。
未来 Python 可能会:
- 默认保留 GIL(兼容性)
- 提供无 GIL 模式(高性能)
这是 Python 历史上最重要的变革之一。
八、总结:GIL 并不是 Python 的“罪人”,而是历史的妥协
本文从多个角度深入解析了 GIL:
✅ GIL 锁住的是解释器状态,而不是 Python 对象
✅ GIL 的本质是保护引用计数与对象模型
✅ 多线程仍然适用于 I/O 密集型任务
✅ C 扩展与多进程可以绕过 GIL
✅ Python 未来可能支持无 GIL 模式
GIL 是 Python 的“保护伞”,也是“枷锁”。理解它,你才能真正写出高性能的 Python 程序。
九、互动时间
我很想听听你的经验:
- 你在 Python 多线程中遇到过哪些坑
- 你认为 GIL 是 Python 的优势还是劣势
- 你希望未来的 Python 如何处理并发
欢迎留言交流,我们一起探索 Python 的更多可能性。

1万+

被折叠的 条评论
为什么被折叠?



