欢迎光临
我们一直在努力

Python GIL 深度解析:它锁住了谁,又究竟保护了什么?

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 实战经验,往往就藏在这些真实踩坑与取舍之中。

    赞(0)
    未经允许不得转载:171主机测评 » Python GIL 深度解析:它锁住了谁,又究竟保护了什么?
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址