Python GIL 深度解析:它锁住了谁,又究竟保护了什么?
提到 Python 多线程,GIL 几乎是一个绕不开的话题。
很多刚接触 Python 并发编程的开发者,都听过这样一句话:
“Python 有 GIL,所以 Python 多线程没有用。”
也有人会进一步理解成:
“既然有 GIL,Python 多线程就是线程安全的。”
遗憾的是,这两句话都不准确。
GIL 确实会限制传统 CPython 中多个线程同时执行 Python 代码,但它并没有让 threading 失去价值;更重要的是,GIL 保护的主要对象并不是你的订单、余额、缓存这些业务数据,而是 Python 解释器自身以及 Python 对象内部状态的一致性。
如果没有理解这一点,在项目中很容易写出这样的代码:
balance = 1000
# 有 GIL,所以多线程访问 balance 应该安全吧?
然后在生产环境里得到一个非常昂贵的答案:
不安全。
本文就来系统拆解这个 Python 编程中经常被误解的概念:
- GIL 是什么?
- GIL 为什么会存在?
- 它真正保护的是什么?
- 为什么有 GIL 仍然会出现线程安全问题?
- 为什么 I/O 密集程序仍然适合多线程?
- 为什么 CPU 密集任务通常不适合传统 CPython 多线程?
- NumPy 为什么又能利用多核?
- Python 3.13、3.14 的 free-threaded Python 又意味着什么?
理解完这些问题,你对 Python 并发编程的认识会清晰很多。
一、GIL 到底是什么?
GIL 全称:
Global Interpreter Lock
中文一般翻译为:
全局解释器锁。
对于传统启用 GIL 的 CPython,可以把它粗略理解为一把解释器级别的大锁:
CPython 进程
Thread A ───────┐
Thread B ───────┼──→ [ GIL ] ──→ Python 对象 / Python C API
Thread C ───────┘
同一时刻,通常只有一个线程能够持有 GIL,并执行相应的 Python 代码、访问 Python 对象和调用需要 GIL 的 Python C API。
Python 官方 C API 文档对此给出的核心解释非常明确:传统 CPython 本身并不是天然线程安全的,因此线程在访问 Python 对象之前需要持有 GIL。(Python documentation)
所以 GIL 首先是一个:
CPython 解释器内部的同步机制。
这里一定要注意一个关键词:
CPython。
Python 是一门语言,而 CPython 是 Python 最主流的实现。
因此,说:
Python 语言规定所有实现都必须有 GIL
是不准确的。
更严谨的说法应该是:
我们平时讨论的 GIL,主要讨论的是 CPython 的实现机制。
二、GIL 为什么会存在?
要理解这个问题,需要先看到 CPython 内部非常重要的一套机制:
引用计数
CPython 对象管理大量依赖引用计数。
例如:
import sys
data = []
print(sys.getrefcount(data))
假设:
a = data
那么 data 又多了一个引用。
粗略理解:
对象 data
│
├── 变量 data
└── 变量 a
引用数量增加
当对象引用计数最终降到 0 时,CPython 通常就可以释放它。
在 C 层面,可以抽象成:
ob_refcnt += 1
和:
ob_refcnt -= 1
问题来了。
假如没有任何同步机制,两个线程同时对同一个 Python 对象的引用计数进行修改:
原引用计数 = 10
Thread A 读取:10
Thread B 读取:10
Thread A 写入:11
Thread B 写入:11
理论上应该变成:
12
结果却可能得到:
11
如果这种错误发生在对象生命周期管理上,后果就不仅仅是“数字算错了”。
它可能意味着:
对象被提前释放
对象迟迟不能释放
内存状态损坏
解释器崩溃
Python 官方文档也正是用“两个线程同时增加同一对象引用计数”为例,解释为什么传统 CPython 需要 GIL。(Python documentation)
所以 GIL 的历史价值非常实际:
用一把全局锁,让大量解释器内部操作不需要分别设计复杂的细粒度并发控制。
三、GIL 真正保护的到底是什么?
这是本文最核心的问题。
很多开发者误以为:
GIL 保护 Python 变量。
严格来说不是。
更准确地说,传统 CPython 中的 GIL主要帮助保护的是:
1. CPython 解释器内部状态
解释器执行 Python 代码过程中,需要不断操作:
对象
引用计数
类型信息
内存管理结构
执行状态
C API
这些结构如果被多个线程完全无约束地同时修改,就需要大量额外同步机制。
2. Python 对象的底层内部一致性
例如:
my_list.append(value)
涉及的不只是“向列表添加一个元素”。
底层还可能涉及:
容量检查
重新分配空间
保存对象指针
修改列表长度
修改引用计数
GIL 使传统 CPython 中很多底层操作天然处于一个较大的串行执行区域内。
3. Python/C API 的访问安全
对于 C 扩展开发者来说,这一点尤其重要。
传统 CPython 中,如果线程要操作:
PyObject *
或者调用大量 Python C API,一般必须拥有相应的线程状态并持有 GIL。
因此可以把 GIL 想象成:
GIL
│
▼
┌──────────────────────────┐
│ CPython Interpreter │
│ │
│ reference counting │
│ Python objects │
│ memory/object state │
│ Python C API │
└──────────────────────────┘
但是注意:
GIL
≠
你的业务锁
这个区别至关重要。
四、最危险的误区:有 GIL,就不需要 Lock?
当然不是。
假设我们开发一个简单的账户系统:
balance = 100
现在两个线程分别尝试扣款 80 元。
代码:
import time
import threading
balance = 100
def withdraw(amount):
global balance
if balance >= amount:
time.sleep(0.01)
balance -= amount
t1 = threading.Thread(target=withdraw, args=(80,))
t2 = threading.Thread(target=withdraw, args=(80,))
t1.start()
t2.start()
t1.join()
t2.join()
print("最终余额:", balance)
你可能期望:
20
因为账户只有 100 元,不应该允许两次 80 元扣款同时成功。
但两个线程可能这样运行:
balance = 100
Thread A:
检查 100 >= 80
结果:True
↓ 切换线程
Thread B:
检查 100 >= 80
结果:True
Thread B:
100 – 80 = 20
↓ 切换线程
Thread A:
20 – 80 = -60
最终:
balance = -60
GIL 去哪里了?
GIL 一直都在。
问题在于:
GIL 没有承诺你整个业务逻辑是一笔不可分割的事务。
这段业务代码包含多个步骤:
读取余额
↓
判断余额
↓
执行其他操作
↓
修改余额
线程完全可能在这些步骤之间发生切换。
所以这里真正需要的是:
lock = threading.Lock()
完整写法:
import threading
balance = 100
lock = threading.Lock()
def withdraw(amount):
global balance
with lock:
if balance >= amount:
balance -= amount
threads = [
threading.Thread(target=withdraw, args=(80,))
for _ in range(2)
]
for thread in threads:
thread.start()
for thread in threads:
thread.join()
print(balance)
这时:
with lock:
保护的是:
你的业务临界区。
因此一定要记住:
GIL → 主要保护解释器内部一致性
Lock → 保护应用程序共享状态的一致性
两者解决的不是同一个层面的问题。
五、为什么 GIL 不等于“一个 Python 操作就是原子的”?
再看一个常见代码:
counter += 1
表面看只有一行。
但“一行 Python 源代码”并不意味着底层只执行一步。
可以自己观察字节码:
import dis
counter = 0
def increase():
global counter
counter += 1
dis.dis(increase)
不同 Python 版本生成的具体字节码可能不同,但核心思想不会变:
读取 counter
执行加法
保存结果
因此:
counter += 1
不能简单理解为:
因为有 GIL,所以它天然就是我的业务原子操作。
更危险的是类似:
if key not in cache:
cache[key] = load_data()
即使某些单独的 dict 操作在特定 CPython 实现下具有内部安全特征,这一整段:
判断
+
计算
+
写入
仍然是组合操作。
两个线程完全可能同时发现:
key 不存在
然后各执行一次:
load_data()
所以线程安全设计的原则应该是:
不要依赖“这几行代码看起来很短”,而要分析共享状态的完整不变量。
六、既然有 GIL,Python 多线程还有什么意义?
非常有意义。
关键在于区分两种任务:
CPU Bound
I/O Bound
即:
CPU 密集型
I/O 密集型
七、I/O 密集任务:多线程依然非常实用
假设程序执行:
response = requests.get(url)
真正耗费的大部分时间可能不是 CPU 计算,而是在等待:
DNS
网络
服务器响应
磁盘
Socket
例如:
Thread A
发送请求
↓
等待网络……………………
此时让 CPU 一直闲着没有意义。
CPython 会在许多阻塞 I/O 场景释放 GIL,从而让其他线程获得执行机会。官方文档也明确指出,GIL 会在文件读取、写入等阻塞 I/O 周围释放。(Python documentation)
所以:
Thread A:等待网络
Thread B:处理响应
Thread C:等待数据库
Thread D:发送请求
能够形成很好的并发效果。
简单案例:
from concurrent.futures import ThreadPoolExecutor
import requests
urls = [
"https://example.com",
"https://example.org",
"https://python.org",
]
def download(url):
response = requests.get(url, timeout=10)
return url, len(response.content)
with ThreadPoolExecutor(max_workers=8) as executor:
results = executor.map(download, urls)
for result in results:
print(result)
对于:
HTTP 请求
数据库访问
文件 I/O
Socket 通信
第三方 API
线程池仍然是非常实用的工具。
因此正确结论不是:
Python 有 GIL,所以不能使用线程。
而应该是:
传统 CPython 的线程通常特别适合 I/O 密集型并发,但纯 Python CPU 密集任务很难通过普通线程充分利用多个 CPU 核心。
Python 官方 threading 文档也给出了相同方向的建议。(Python documentation)
八、CPU 密集任务:为什么线程通常加速不了?
假设:
def cpu_work(n):
total = 0
for i in range(n):
total += i * i
return total
这里几乎没有等待。
CPU 一直执行 Python 代码。
如果创建四个线程:
Thread 1 ─┐
Thread 2 ─┤
Thread 3 ─┼──→ GIL ──→ Python code
Thread 4 ─┘
传统 GIL 模式下,它们需要竞争解释器执行权。
结果很可能是:
不是 4 个核心同时高效执行 Python 循环
而是多个线程轮流执行
甚至因为:
线程调度
上下文切换
锁竞争
运行时间可能比单线程还差。
可以自己测试:
import time
from concurrent.futures import ThreadPoolExecutor
def cpu_work(n):
total = 0
for i in range(n):
total += i * i
return total
N = 8_000_000
start = time.perf_counter()
for _ in range(4):
cpu_work(N)
print(
"单线程:",
time.perf_counter() – start
)
start = time.perf_counter()
with ThreadPoolExecutor(max_workers=4) as executor:
list(executor.map(cpu_work, [N] * 4))
print(
"4线程:",
time.perf_counter() – start
)
具体结果会受到:
Python 版本
CPU
操作系统
后台负载
影响,所以不要把某一次测试数字当成绝对规律。
但对于传统 GIL 模式下的纯 Python CPU 密集循环,线程通常不会带来期望中的多核线性加速。
九、CPU 密集任务怎么办?用多进程
传统 CPython 中,一个经典方案是:
ProcessPoolExecutor
例如:
from concurrent.futures import ProcessPoolExecutor
def cpu_work(n):
total = 0
for i in range(n):
total += i * i
return total
if __name__ == "__main__":
jobs = [
8_000_000,
8_000_000,
8_000_000,
8_000_000,
]
with ProcessPoolExecutor() as executor:
results = list(
executor.map(cpu_work, jobs)
)
print(results)
为什么多进程可以?
因为:
Process A → Python Interpreter A → GIL A
Process B → Python Interpreter B → GIL B
Process C → Python Interpreter C → GIL C
Process D → Python Interpreter D → GIL D
它们不是共同争抢一个进程里的同一把 GIL。
于是操作系统可以把不同进程真正调度到多个 CPU 核心上。
代价则是:
进程创建成本更高
内存消耗更多
数据需要序列化
进程间通信更复杂
因此工程中通常这样选择:
| HTTP/API 调用 | threading / asyncio |
| 数据库 I/O | threading / asyncio |
| 文件 I/O | threading / asyncio |
| 大量纯 Python 数值计算 | multiprocessing |
| CPU 密集任务池 | ProcessPoolExecutor |
| 大量高并发网络连接 | asyncio |
| 混合型系统 | asyncio + thread/process pool |
十、一个有趣的问题:NumPy 为什么能利用多核?
这时很多开发者会发现一个矛盾。
Python 有 GIL,但:
NumPy
SciPy
PyTorch
TensorFlow
为什么很多计算却能跑满多个 CPU 核心?
原因是:
GIL 限制的是持有 GIL 执行 Python 相关代码的线程,不代表所有本地机器代码永远只能单线程执行。
许多高性能库的核心计算实际上发生在:
C
C++
Fortran
BLAS
OpenMP
CUDA
里面。
当 C 扩展进入一段不需要操作 Python 对象的长时间计算时,可以主动释放 GIL。
Python 官方 C API 文档就提到,除了阻塞 I/O,一些长时间执行、但不需要访问 Python 对象的本地代码也可以释放相应线程状态;标准库中的 zlib、hashlib 就包含类似做法。(Python documentation)
于是可能形成:
Python Thread A
│
▼
调用 NumPy
│
释放 GIL
▼
C / BLAS
├── Core 1
├── Core 2
├── Core 3
└── Core 4
这也解释了为什么一句:
“Python 有 GIL,所以 Python 永远不能进行并行计算。”
是错误的。
十一、并发和并行一定要分清楚
这是理解 GIL 的另一个关键。
并发 Concurrency
多个任务在同一个时间段内持续推进:
时间 ─────────────────────→
Task A ███ ███
Task B ███ ███
Task C ███
它们未必真的在同一个物理时刻执行。
并行 Parallelism
多个任务在同一个时刻真正运行:
CPU Core 1 █████████
CPU Core 2 █████████
CPU Core 3 █████████
CPU Core 4 █████████
所以传统 CPython:
threading
可以很好地实现:
并发
特别是 I/O 场景。
但对于:
纯 Python CPU 密集代码
传统 GIL 会限制线程之间真正的 CPU 并行。
十二、GIL 会一直锁着不放吗?
不会。
传统 CPython 会在适当时机让线程之间获得执行机会。
还可以查看线程切换间隔:
import sys
print(sys.getswitchinterval())
以及设置:
sys.setswitchinterval(0.01)
但是请不要把这个参数当成普通程序的“多线程性能调节旋钮”。
绝大多数应用没有必要修改它。
此外,遇到:
blocking I/O
或者某些主动释放 GIL 的 C 扩展代码时,GIL 也可能被释放。
所以实际运行更接近:
Thread A 获取 GIL
↓
执行 Python
↓
释放 / 被切换
Thread B 获取 GIL
↓
执行 Python
↓
释放 / 被切换
而不是某一个线程从程序开始一直锁到程序结束。
十三、Python 正在发生巨大变化:Free-Threaded Python
如果几年前写 GIL 教程,到这里差不多可以结束了。
但今天已经不能这样写。
Python 的多线程模型正在经历一次非常重要的改变。
Python 3.13:可选 free-threaded build
PEP 703 提出了:
让 CPython 的 GIL 变成可选机制。
Python 3.13 开始提供 free-threaded 构建,使 CPython 可以在关闭 GIL 的模式下运行,让多个线程真正并行执行 Python 代码。(Python Enhancement Proposals (PEPs))
可以理解成:
传统模型:
Thread A ─┐
Thread B ─┼── GIL ─→ Python
Thread C ─┘
Free-threaded 模型:
Thread A ──→ Core 1
Thread B ──→ Core 2
Thread C ──→ Core 3
Thread D ──→ Core 4
当然,实现它远不是“把 GIL 删除”这么简单。
PEP 703 涉及大量 CPython 内部改造,包括:
引用计数
内存管理
容器线程安全
锁与原子操作
否则直接把原来的 GIL 删除,解释器本身就可能失去线程安全。(Python Enhancement Proposals (PEPs))
十四、Python 3.14:Free-Threaded 从实验走向正式支持
这里尤其值得关注。
Python 3.13 中 free-threaded CPython 仍处于实验阶段,而到了 Python 3.14,PEP 779 推动它进入 Phase II:
free-threaded Python 已正式成为受支持的构建方式,但仍然是可选模式,而不是默认模式。(Python Enhancement Proposals (PEPs))
因此截至当前 Python 3.14 系列,我们不能简单说:
Python 已经完全没有 GIL
也不能说:
Python 永远都会有 GIL
更准确的描述是:
传统 GIL 构建仍然存在并广泛使用,同时 CPython 已正式支持可选的 free-threaded 构建。
这也是学习 Python 并发时必须更新的知识。
十五、如何判断当前 Python 是否启用了 GIL?
在支持相关功能的 CPython 中,可以检查:
import sys
print(sys._is_gil_enabled())
可能得到:
True
说明当前运行时启用了 GIL。
如果使用 free-threaded Python 并实际关闭了 GIL,则可能得到:
False
也可以查看当前构建:
import sysconfig
print(
sysconfig.get_config_var(
"Py_GIL_DISABLED"
)
)
官方 free-threading 文档建议使用 Py_GIL_DISABLED 对构建配置进行判断,而 sys._is_gil_enabled() 可以查看当前运行进程中 GIL 是否实际启用。(Python documentation)
十六、没有 GIL,是不是以后就不用 Lock 了?
恰恰相反。
这是 free-threaded Python 时代最需要建立的新认识之一。
假设:
inventory = 1
两个线程:
if inventory > 0:
inventory -= 1
即使 Python 完全没有 GIL,也不意味着:
检查库存 + 修改库存
突然变成业务事务。
反而由于真正的多线程并行增加,这类共享状态问题会更加值得重视。
Free-threaded CPython 为 dict、list、set 等内建类型加入了内部同步机制,以维持类似传统 GIL 构建的底层安全行为,但官方仍明确建议:应用程序应使用 threading.Lock 等同步工具,而不是依赖内建类型内部锁作为自己的业务同步方案。(Python documentation)
因此未来正确的开发思维应该是:
解释器线程安全
≠
应用程序线程安全
这一点无论有没有 GIL 都不会改变。
十七、Free-Threaded 为什么这么难?
因为过去几十年,很多 CPython 内部代码和 C 扩展事实上默认:
同一时刻只有一个线程操作 Python 对象
GIL 在某种程度上承担了一层“隐式保护”。
一旦取消这把大锁,就必须重新处理:
引用计数竞争
容器并发修改
内存分配
全局缓存
扩展模块内部状态
对象生命周期
垃圾回收
例如 C 扩展过去有:
global_cache
可能从来没有加过锁。
为什么?
因为过去默认调用相关 Python C API 时有 GIL。
到了 free-threaded 环境,这个假设可能不再成立。
Python 官方针对 C 扩展的迁移文档也专门提醒:
原来由 GIL 隐式保护的扩展内部缓存和全局状态,现在可能需要自己增加锁或改用线程局部存储。(Python documentation)
这句话其实非常准确地回答了本文标题:
GIL 到底保护了什么?
它长期以来不仅限制并行,也实际上替大量 CPython 内部代码和扩展代码承担了一部分全局同步责任。
十八、实战中到底该怎样选择并发方案?
理解 GIL 最终不是为了背面试题,而是为了做架构决策。
我更推荐从任务性质出发。
场景一:大量 HTTP 请求
例如:
爬虫
API 聚合
批量下载
第三方接口
优先:
asyncio
ThreadPoolExecutor
场景二:数据库、文件等阻塞 I/O
可以考虑:
threading
异步驱动
asyncio.to_thread()
场景三:纯 Python CPU 密集计算
传统 GIL 构建下优先:
ProcessPoolExecutor
multiprocessing
例如:
from concurrent.futures import ProcessPoolExecutor
def calculate(data):
return sum(
value * value
for value in data
)
if __name__ == "__main__":
datasets = [
range(1_000_000),
range(1_000_000),
range(1_000_000),
range(1_000_000),
]
with ProcessPoolExecutor() as pool:
results = list(
pool.map(calculate, datasets)
)
print(results)
场景四:NumPy / PyTorch 等计算
先检查库本身是否已经:
释放 GIL
使用 BLAS
使用 OpenMP
使用 GPU
不要为了“多核”再盲目套一层 Python 线程。
场景五:准备拥抱 Free-Threaded Python
重点检查:
共享可变状态
第三方 C 扩展兼容性
隐式原子性假设
全局缓存
线程局部变量
业务临界区
性能基准
free-threaded 不是:
把 Python 换个解释器,然后线程数改成 32。
而是:
重新认真审视你的并发模型。
十九、关于 GIL,记住这 8 句话就够了
如果整篇文章只留下几个结论,我希望是这些:
GIL 是 CPython 的解释器级同步机制,不是 Python 语言本身的业务锁。
传统 GIL 构建中,同一时刻通常只有一个线程执行相关 Python 代码。
GIL 的核心历史作用之一,是帮助保护 CPython 对象、引用计数以及解释器内部状态的一致性。
GIL 不保证你的业务逻辑线程安全。
有 GIL 仍然需要 Lock、RLock、Queue 等同步机制。
I/O 密集任务依然非常适合线程,因为阻塞 I/O 可以释放 GIL。
传统 GIL 模式下,纯 Python CPU 密集代码通常应考虑多进程,而不是盲目增加线程。
从 Python 3.13 开始 CPython 提供 free-threaded 构建;Python 3.14 已将其推进到正式支持但仍可选的阶段。
二十、结语:理解 GIL,比讨厌 GIL 更重要
学习 Python 一段时间后,我们很容易把 GIL 当成一个“限制 Python 性能的坏东西”。
但如果真正深入 CPython,就会发现事情远没有这么简单。
GIL 给 Python 带来了限制:
纯 Python 多线程 CPU 并行受限
但在很长时间里,它也降低了:
解释器实现复杂度
C 扩展同步复杂度
对象管理成本
如今,随着 PEP 703 和 free-threaded Python 的推进,CPython 正在尝试跨过这条历史边界。
这意味着未来的 Python 编程可能拥有更强的原生多线程并行能力,但开发者同时也必须更认真地面对:
Race Condition
Deadlock
Lock Contention
Shared Mutable State
Thread Safety
换句话说:
GIL 的逐步可选化,不是“并发问题消失了”,而是 Python 开发者终于能够获得更强的并行能力,同时也需要承担更多真正的并发设计责任。
如果你是初学者,不必因为 GIL 而害怕 Python 多线程。
先学会判断:
这是 CPU Bound?
还是 I/O Bound?
如果你已经是资深 Python 开发者,那么现在则是一个非常值得关注的时间节点:
Python 多线程的传统经验正在发生变化。
过去我们经常问:
“怎样绕过 GIL?”
未来越来越值得问的问题可能是:
“当 GIL 不再替我承担那部分隐式同步责任以后,我的代码真的线程安全吗?”
而这,可能才是理解 GIL 最有价值的地方。
SEO 关键词
Python编程、Python教程、Python实战、Python最佳实践、Python GIL、Python多线程、Python并发编程、threading、multiprocessing、CPU密集型、I/O密集型、Free-Threaded Python、PEP 703、Python 3.14。
推荐延伸阅读
建议继续阅读以下官方资料:
- Python threading 官方文档;
- Python C API:Thread State 与 Global Interpreter Lock;
- PEP 703:Making the Global Interpreter Lock Optional in CPython;
- PEP 779:Free-Threaded Python Supported Status;
- Python Free-Threading 使用指南;
- Python multiprocessing 与 concurrent.futures 文档。
同时推荐进一步阅读《流畅的 Python》《Effective Python》以及《Python 编程:从入门到实践》,逐步把线程、协程、多进程和 Python 对象模型联系起来理解。
互动讨论
你在项目中是否遇到过这种情况:
明明用了 Python 多线程,CPU 利用率却始终上不去?
或者因为误以为“有 GIL 就线程安全”,最终遇到了共享数据竞态问题?
面对已经进入正式支持阶段的 free-threaded Python,你会愿意在生产项目中尝试它吗?
欢迎分享你的实践经验。很多并发问题没有一个放之四海而皆准的答案,而真正有价值的 Python 实战经验,往往就藏在这些真实踩坑与取舍之中。





