Python 3.13 的自由线程(no-GIL)是什么?GIL 真的要没了吗?
简化版
「自由线程(free-threading)」是 Python 3.13 通过 PEP 703 引入的一个可选构建版本**——它真正移除了 GIL,让多个线程能在多核上同时执行 Python 字节码,从此 CPU 密集型任务用多线程也能加速。注意三个关键限定:① 它是「另一个构建版本」而不是默认版本——普通 python3.13 仍然带 GIL,自由线程版叫 python3.13t(可执行文件带 t 后缀),需要单独安装;② 3.13 里它是「实验性」的(3.14 起转为「官方支持但仍非默认」),③ 单线程性能有损失(3.13 上约 5%~10%,3.14 已优化到很小)。移除 GIL 不是「删掉一把锁」那么简单——CPython 的引用计数本身就需要保护,PEP 703 的做法是引入偏向引用计数(biased reference counting)、不朽对象(immortal objects,如 None/True/小整数不再计数)、以及对 dict/list 等容器的细粒度锁和无锁读取**。对写 Python 的人意味着什么:以前「多线程只对 IO 有用、CPU 密集必须上多进程」的铁律被打破,但代价是竞态条件会真正暴露——过去 GIL 让很多不安全的代码「碰巧没出问题」,没了 GIL 之后 counter += 1 这类操作的竞态会实实在在地发生。另外 C 扩展必须适配(numpy、pandas 等已陆续支持),未适配的扩展在自由线程版里会自动重新启用 GIL。核心记忆:no-GIL 是可选构建、3.13 实验性、单线程有损耗、加锁的责任回到了你自己身上。
详细版
三种运行形态对比:
| 维度 | 标准 CPython(带 GIL) | 自由线程构建(python3.13t) | 多进程 |
|---|---|---|---|
| CPU 密集多线程加速 | ❌ 无(GIL 串行) | ✅ 真正并行 | ✅ |
| 内存开销 | 低 | 低(共享堆) | 高(每进程一份) |
| 数据共享 | 直接共享对象 | 直接共享对象 | 要序列化(pickle) |
| 启动开销 | 极低 | 极低 | 高(尤其 spawn) |
| 竞态风险 | GIL 掩盖了一部分 | ⚠️ 完全暴露 | 天然隔离 |
| 单线程性能 | 基准 | 3.13 慢 5%~10% | 基准 |
| C 扩展兼容 | 全部 | 需适配(否则退回 GIL) | 全部 |
import sys, sysconfig, threading, time
# ① 判断当前解释器是不是自由线程构建
print(sysconfig.get_config_var("Py_GIL_DISABLED")) # 1 = 自由线程构建,None/0 = 标准构建
print(sys._is_gil_enabled()) # ★3.13+:GIL 当前是否启用★
# 自由线程版里加载了未适配的 C 扩展 → 会自动重新打开 GIL,这个函数会返回 True
# ② CPU 密集任务的对照实验
def cpu_work(n=10_000_000):
s = 0
for i in range(n):
s += i * i
return s
def bench(nthreads):
t0 = time.perf_counter()
ts = [threading.Thread(target=cpu_work) for _ in range(nthreads)]
for t in ts: t.start()
for t in ts: t.join()
return time.perf_counter() - t0
# 标准 CPython:bench(1)≈2.0s,bench(4)≈8.0s ← ★4 倍工作量、4 倍时间,没有加速★
# 自由线程构建:bench(1)≈2.1s,bench(4)≈2.3s ← ★接近线性加速★
# ③ ★没了 GIL,竞态会真正暴露★
counter = 0
def bump():
global counter
for _ in range(1_000_000):
counter += 1 # ★读-改-写三步,不是原子操作★
ts = [threading.Thread(target=bump) for _ in range(4)]
for t in ts: t.start()
for t in ts: t.join()
print(counter)
# 标准 CPython:★通常★是 4000000(GIL 掩盖了大部分竞态,但★不保证★)
# 自由线程构建:★几乎必然小于 4000000★(真正并行 → 丢失更新)
# ④ 正确写法:显式加锁(★两种构建下都正确★)
lock = threading.Lock()
def bump_safe():
global counter
for _ in range(1_000_000):
with lock:
counter += 1
# 或用无锁的原子计数(itertools.count 的 next 在 CPython 里是原子的,但★别依赖★)
# ⑤ 运行时开关(3.13)
# python3.13t app.py 默认关闭 GIL
# PYTHON_GIL=1 python3.13t app.py ★强制打开 GIL★(排查问题时用)
# python3.13t -X gil=1 app.py 同上
⚠️ 三个最容易误解的点:① 「Python 3.13 移除了 GIL」这个说法不准确——3.13 提供的是一个可选的、独立的构建版本(
python3.13t),默认安装的python3.13仍然有 GIL 且行为不变。PEP 703 明确规划了多年的过渡期:3.13 实验性 → 3.14 官方支持(仍非默认)→ 未来某个版本才可能成为默认,而且随时可能因为生态适配不佳而回退。② 没有 GIL 不等于不用加锁——恰恰相反,GIL 过去掩盖了大量竞态:counter += 1、list的检查后修改、字典的if k not in d: d[k] = v这类「读-改-写」操作在 GIL 下碰巧很少出错(字节码之间的切换点有限),移除后会真实地丢失更新。所有共享可变状态都必须显式用Lock/Queue保护。③ C 扩展是最大的现实约束:未声明支持自由线程的扩展一旦被导入,解释器会自动重新启用 GIL(并发出警告),你的「无 GIL」就白搭了——所以真正能享受收益要等整条依赖链都适配完成。
完整版教学
一、为什么移除 GIL 这么难
GIL(全局解释器锁)到底在保护什么:
① ★引用计数★(最核心)
每个 PyObject 都有 ob_refcnt 字段,赋值/传参/离开作用域都要 ++/--
→ 多线程同时改一个对象的计数 = ★竞态 → 提前释放(崩溃)或永不释放(泄漏)★
→ 而引用计数操作★极其频繁★(每秒上亿次),用互斥锁保护会慢到不可接受
② 内置容器的内部结构
list 扩容时的 realloc、dict 的哈希表 rehash 都不是原子的
③ 解释器的全局状态(内存分配器、类型对象缓存、模块表…)
④ C 扩展的隐含假设:★大量 C 扩展代码假定"同一时刻只有一个线程在跑 Python"★
→ 所以 GIL 不是"一个多余的锁",而是"用一把大锁换来的简单性和单线程性能"
历史上的多次尝试都失败了:
1999 Greg Stein 的 free-threading 补丁:单线程性能★慢一倍★ → 被拒
2015 Larry Hastings 的 Gilectomy:同样卡在引用计数的原子操作开销
→ ★核心矛盾:细粒度锁 = 单线程变慢;粗粒度锁 = 无法并行★
PEP 703(Sam Gross)为什么这次成了 —— 四项关键技术:
① ★偏向引用计数(biased reference counting)★
每个对象记住"拥有它的线程";
拥有者线程改计数用★非原子★操作(快),其他线程改计数才用原子操作
→ 绝大多数对象只被一个线程碰 → ★绝大多数计数操作保持原速★
② ★不朽对象(immortal objects,PEP 683)★
None / True / False / 小整数 / 内置类型 / interned 字符串
→ 计数字段设为特殊值,★永不增减★ → 消除了最热门对象的计数争抢
③ ★延迟引用计数(deferred refcounting)★
顶层函数、模块、类等长生命周期对象,在解释器栈上的引用不立刻计数
④ ★内部锁细化 + 无锁读★
dict/list 用 per-object 锁 + 保证读操作无锁(借鉴了 mimalloc 分配器)
⑤ 换用 mimalloc 内存分配器(支持无锁的线程本地分配)
代价(★必须知道★):
单线程性能:3.13 上约 ★慢 5%~10%★(早期版本更多),3.14 已优化到接近持平
内存占用:每个对象的头部变大(多了线程 id 等字段)
C 扩展要改:Py_INCREF 等宏的语义变了
理解自由线程的前提是理解GIL 到底在保护什么——它不是「一个多余的锁」,而是用一把大锁换来的简单性和单线程性能。最核心的保护对象是引用计数:每个 Python 对象都有 ob_refcnt,赋值、传参、离开作用域都要增减,频率高达每秒上亿次——如果每次都用原子操作或互斥锁保护,单线程性能会崩塌。这正是历史上多次移除 GIL 的尝试(1999 年 Greg Stein 的补丁、2015 年的 Gilectomy)失败的原因:细粒度锁让单线程变慢,粗粒度锁又无法并行。PEP 703 这次能成功靠的是四项技术:偏向引用计数(对象记住「拥有者线程」,拥有者用非原子操作改计数,只有跨线程访问才用原子操作——而绝大多数对象只被一个线程碰)、不朽对象(None、True、小整数等永不增减计数,消除最热门对象的争抢)、延迟引用计数、以及 dict/list 的细粒度锁 + 无锁读取。代价是单线程慢 5%~10%(3.14 已接近持平)和 C 扩展必须适配。
二、自由线程构建怎么用
安装(★它是独立的构建,不是开关★):
官方安装包:Windows/macOS 安装器里勾选 "free-threaded binaries"
pyenv: pyenv install 3.13.0t ★注意 t 后缀★
uv: uv python install 3.13t
Docker: python:3.13-rc-slim 系列有对应 tag
源码编译: ./configure --disable-gil && make
→ 得到的可执行文件是 ★python3.13t★(Windows 上是 python3.13t.exe)
→ 它和普通 python3.13 ★可以共存★,虚拟环境要分开建
判断当前环境:
import sysconfig, sys
sysconfig.get_config_var("Py_GIL_DISABLED") # 1 = 自由线程构建
sys._is_gil_enabled() # ★运行时 GIL 是否启用★(3.13+)
sys.version # 含 "free-threading build" 字样
★ 两者的区别很重要:
构建支持无 GIL ≠ 当前运行时真的没有 GIL
→ 加载了未适配的 C 扩展 → ★运行时自动打开 GIL★(并 warning)
运行时控制:
PYTHON_GIL=0/1 环境变量
-X gil=0/1 命令行
★ 排查问题的标准手段:怀疑是并发 bug 时,用 PYTHON_GIL=1 跑一遍
→ 如果问题消失,基本可以确定是竞态
C 扩展的适配(写扩展的人才需要):
模块要声明支持:PyUnstable_Module_SetGIL(m, Py_MOD_GIL_NOT_USED)
或在 multi-phase init 的 slot 里声明
→ 未声明的扩展被 import 时:
RuntimeWarning: The global interpreter lock (GIL) has been enabled to load
module 'xxx', which has not declared that it can run safely without the GIL.
★ 可以用 PYTHONWARNINGS 或 -X gil=0 强制不启用(★风险自负★)
生态现状(★面试时能说出具体进展才显得跟进过★):
已支持:numpy(2.1+)、Cython(3.1+)、pybind11、scikit-learn、pillow、
lxml、cryptography 等主流科学/基础库
过渡中:pandas、pytorch 等大型项目
★ 关键判断:你的依赖链★全部★适配了才能真正跑在无 GIL 下
★ pip 会为自由线程版下载带 cp313t 标签的 wheel(没有就现场编译)
自由线程是独立的构建版本而不是一个开关:安装后得到的可执行文件是 python3.13t(带 t 后缀),和普通 python3.13 可以共存但虚拟环境要分开建(pyenv install 3.13.0t、uv python install 3.13t)。判断环境时要分清两件事:sysconfig.get_config_var("Py_GIL_DISABLED") 说明「这个构建支持无 GIL」,而 sys._is_gil_enabled() 才说明「此刻 GIL 是否真的关着」——因为加载了未适配的 C 扩展会让解释器自动重新打开 GIL(并发出 RuntimeWarning)。运行时可以用 PYTHON_GIL=1 或 -X gil=1 强制打开 GIL,这是排查并发 bug 的标准手段:如果开了 GIL 问题就消失,基本可以确定是竞态。生态方面,numpy 2.1+、Cython 3.1+、pybind11、pillow、cryptography 等已支持,关键是你的整条依赖链都适配了才能真正享受收益。
三、没了 GIL,你的代码会怎样
★ 好消息:CPU 密集型多线程真的能加速了
4 核机器上跑 4 个纯计算线程:
标准 CPython: 1 线程 2.0s → 4 线程 ★8.0s★(总工作量 4 倍,时间也 4 倍)
自由线程构建: 1 线程 2.1s → 4 线程 ★2.3s★(接近线性加速)
→ 图像处理、数值计算、压缩、加密这类任务不必再上多进程
★ 坏消息:过去"碰巧没出问题"的代码会开始出问题
GIL 时代的真相:
GIL 保证"同一时刻只有一个线程执行字节码",
但★不保证一条 Python 语句是原子的★
counter += 1 编译成:LOAD → ADD → STORE 三条字节码
→ 线程可能在中间被切走(每 5ms 或遇到阻塞调用)
→ ★竞态一直存在,只是概率低★(切换点少、窗口小)
没了 GIL:
→ 真正的并行执行 → ★窗口从"偶尔"变成"随时"★
→ 同样的代码,丢失更新从"百万分之一"变成"几乎必然"
实测(4 线程各 +100 万):
标准 CPython: 结果通常正好 4000000(但★不保证★,加大循环或加 sleep 就会错)
自由线程构建: 结果常在 ★1500000~3000000★ 之间
★ 哪些操作在两种构建下都不安全(要自己加锁):
x += 1 / x = x + 1 读-改-写
if k not in d: d[k] = v ★检查后修改★
lst[i] = lst[i] + 1
两个相关变量的同步更新(如"扣库存 + 记流水")
任何需要"多步保持一致"的逻辑
★ 哪些仍然安全:
Queue.put / get ★内部有锁,两种构建都安全★
Lock / RLock / Semaphore 本来就是同步原语
单个 list.append(x) ★CPython 实现上是原子的★(但★不是语言保证★)
dict[k] = v(单次赋值) 同上
→ ★结论:不要靠"哪些操作碰巧原子"来写代码,一律用 Lock 或 Queue★
写代码的实际影响:
① ★所有共享可变状态必须显式加锁★(这本来就是对的,只是现在必须做对)
② 优先用"不共享"的设计:Queue 传递、每线程独立数据、纯函数
③ 库作者要审视:模块级可变状态、缓存、单例的线程安全性
④ 测试:并发测试要在自由线程构建下跑,才能暴露真实竞态
对写 Python 的人来说,自由线程带来一好一坏两个变化。好消息是 CPU 密集型多线程真的能加速了——4 核上跑 4 个计算线程,标准 CPython 要 8 秒(总工作量 4 倍、时间也 4 倍),自由线程构建只要 2.3 秒(接近线性)。坏消息是过去「碰巧没出问题」的代码会开始出问题。这里要澄清一个普遍误解:GIL 从来就不保证「一条 Python 语句是原子的」——counter += 1 编译成 LOAD/ADD/STORE 三条字节码,线程随时可能在中间被切走,竞态一直存在,只是概率低(切换点少、窗口小)。没了 GIL 之后,窗口从「偶尔」变成「随时」,同样的代码丢失更新从百万分之一变成几乎必然(实测 4 线程各加 100 万,结果常在 150 万~300 万之间)。结论不是「学会哪些操作碰巧原子」,而是「所有共享可变状态一律用 Lock 或 Queue 保护」——这本来就是正确写法,只是现在必须做对。
四、和多进程、asyncio 的选型关系
三种并发模型在"自由线程时代"的重新定位:
┌──────────────┬────────────────┬────────────────┬────────────────┐
│ │ 多线程(有 GIL) │ ★多线程(无 GIL)★│ 多进程 │
├──────────────┼────────────────┼────────────────┼────────────────┤
│ CPU 密集 │ ❌ 无加速 │ ✅ ★线性加速★ │ ✅ 线性加速 │
│ IO 密集 │ ✅ │ ✅ │ ✅(但太重) │
│ 内存开销 │ 低 │ 低 │ ★高★(各一份堆)│
│ 启动开销 │ 微秒级 │ 微秒级 │ ★毫秒~秒级★ │
│ 数据共享 │ 直接 │ 直接 │ ★要 pickle★ │
│ 隔离性/健壮性 │ 一崩全崩 │ 一崩全崩 │ ★进程隔离★ │
│ 竞态风险 │ 被 GIL 部分掩盖 │ ⚠️ ★完全暴露★ │ 天然隔离 │
└──────────────┴────────────────┴────────────────┴────────────────┘
★ 自由线程最大的价值不是"比多进程快",而是★共享内存不用序列化★:
多进程处理一个 2GB 的数组:要么 pickle 传输(慢且翻倍内存),
要么用 shared_memory(麻烦);
无 GIL 多线程:★直接共享同一个对象,零拷贝★
选型建议(自由线程成熟后):
IO 密集 + 高并发 → ★asyncio★(单线程、开销最小)
IO 密集 + 用同步库 → 线程池
CPU 密集 + ★数据量大/共享多★ → ★无 GIL 多线程★(零拷贝是杀手锏)
CPU 密集 + 任务独立/要隔离 → 多进程(一个任务崩了不影响别人)
需要用未适配的 C 扩展 → 多进程(无 GIL 下它会强制打开 GIL)
★ asyncio 会被取代吗:不会
asyncio 的价值是"用极少的资源支撑海量并发连接"(10 万连接 vs 10 万线程)
→ 无 GIL 不改变"线程有栈、有调度开销"这个事实
→ ★两者互补★:无 GIL 让 asyncio 程序里的 CPU 密集部分可以用线程池真正并行
(以前 run_in_executor 处理 CPU 任务是没用的,只能用进程池)
★ 一个重要的现实提醒:
无 GIL ≠ 自动变快
- 单线程程序:★反而慢 5%~10%★
- 只有"多线程 + CPU 密集 + 依赖链全适配"三个条件同时满足才有收益
- 而且要处理真实的竞态问题(开发成本上升)
自由线程改变了并发模型的选型格局,但它最大的价值不是「比多进程快」,而是「共享内存不用序列化」:多进程处理一个 2GB 数组要么 pickle 传输(慢且内存翻倍)、要么用 shared_memory(麻烦),而无 GIL 多线程直接共享同一个对象、零拷贝。选型上:IO 密集高并发仍然是 asyncio 的地盘(它的价值是「用极少资源支撑海量连接」,无 GIL 不改变「线程有栈和调度开销」这个事实);CPU 密集且数据共享多用无 GIL 多线程;CPU 密集但任务独立、需要故障隔离仍然选多进程(一个任务崩了不影响别人)。两者其实是互补的:无 GIL 让 asyncio 程序里的 CPU 密集部分终于可以用线程池真正并行(以前 run_in_executor 处理 CPU 任务毫无意义,只能上进程池)。最后一个现实提醒:无 GIL 不等于自动变快——单线程反而慢 5%~10%,只有「多线程 + CPU 密集 + 依赖链全适配」三个条件同时满足才有收益,还要付出处理真实竞态的开发成本。
五、迁移与实践建议
现在(2026 年)该怎么做:
① ★生产环境:还不要切★
3.13 的自由线程是实验性的;3.14 转为官方支持但仍非默认
依赖链适配不全、性能特征还在变、生产事故成本高
✓ 但可以在 CI 里加一个自由线程的测试任务(提前发现问题)
② ★现在就该做的事:把代码写对★
- 所有共享可变状态用 Lock 保护 或 改用 Queue 传递
- 消除模块级可变全局状态(改成显式传参或线程本地)
- 库作者:审视缓存、单例、延迟初始化的线程安全性
★ 这些在有 GIL 的今天也是对的,只是现在多了一个"必须做对"的理由
③ 库作者的适配清单:
□ 纯 Python 库:审查共享状态 + 在自由线程下跑测试
□ C 扩展:声明 Py_MOD_GIL_NOT_USED + 审查全局状态 + 用原子操作
□ Cython:升级到 3.1+ 并开启 freethreading_compatible
□ 发布 cp313t 的 wheel
④ 测试并发正确性的手段:
- 在自由线程构建下跑现有测试套件(★很多竞态会直接暴露★)
- 用 sys.setswitchinterval(0.000001) 提高切换频率(有 GIL 时也能放大竞态)
- 压力测试:多线程 × 大循环 × 断言最终一致性
- ThreadSanitizer(C 扩展作者)
⑤ 面试怎么答(★要点式★):
"3.13 通过 PEP 703 提供了★可选的★自由线程构建(python3.13t),
真正移除 GIL;靠偏向引用计数、不朽对象、细粒度锁解决了引用计数的竞态。
代价是单线程慢 5%~10%、C 扩展要适配(未适配会自动重新启用 GIL)。
3.13 实验性、3.14 官方支持但非默认。
对业务代码的影响是:CPU 密集多线程能加速了,但★GIL 掩盖的竞态会暴露★,
共享可变状态必须显式加锁。"
时间线(记住大致节奏就够):
2023-07 指导委员会接受 PEP 703
2024-10 Python 3.13 发布,★实验性★自由线程构建
2025-10 Python 3.14 发布,★官方支持★(仍非默认),单线程性能大幅改善
未来 视生态适配情况,可能成为默认(★也可能回退★)
现在(2026 年)的务实建议是:生产环境还不要切(3.13 实验性、3.14 官方支持但仍非默认,依赖链适配不全),但可以在 CI 里加一个自由线程的测试任务提前发现问题。现在就该做的是把代码写对——所有共享可变状态用 Lock 保护或改用 Queue 传递、消除模块级可变全局状态、库作者审视缓存和单例的线程安全性;这些在有 GIL 的今天本来就是正确做法,只是现在多了一个「必须做对」的理由。有个很实用的技巧:在有 GIL 的环境下用 sys.setswitchinterval(0.000001) 提高线程切换频率,能显著放大竞态、让隐藏的 bug 暴露出来。时间线记住大致节奏即可:2023 年 PEP 703 被接受 → 2024 年 3.13 实验性 → 2025 年 3.14 官方支持(仍非默认)→ 未来视生态情况可能成为默认,也可能回退。
六、常见疑问澄清
Q:GIL 是 Python 语言的特性吗?
A:★不是★,是 CPython 这个实现的特性。
Jython(JVM)、IronPython(.NET)从来就没有 GIL;
PyPy 有 GIL 但也有 STM 的实验分支。
→ 面试时说"Python 有 GIL"不严谨,应该说"CPython 有 GIL"
Q:GIL 是不是意味着 Python 多线程完全没用?
A:★不是★。GIL 在以下时刻会被释放:
- 执行 IO 系统调用(read/write/recv/accept…)
- time.sleep()
- 部分 C 扩展的计算(numpy 的大矩阵运算★会释放 GIL★)
- 每 sys.getswitchinterval()(默认 5ms)主动切换
→ 所以 IO 密集型多线程一直是有效的,这也是"IO 用线程、CPU 用进程"的由来
Q:无 GIL 之后,asyncio 还有意义吗?
A:★有★。asyncio 解决的是"海量并发连接"(10 万连接用 10 万线程不现实:
每线程 8MB 栈 + 调度开销),无 GIL 不改变这一点。
两者互补:asyncio 管连接,线程池管 CPU 计算。
Q:自由线程会让我的程序自动变快吗?
A:★不会★。单线程程序反而★慢 5%~10%★(3.13)。
只有"多线程 + CPU 密集"才有收益,而且要求依赖链全部适配。
Q:为什么不像 Java 那样一开始就做细粒度锁?
A:因为 CPython 用★引用计数★做内存管理(Java 用 GC)。
引用计数的读写频率极高(每秒上亿次),细粒度保护的开销无法接受。
PEP 703 正是通过"偏向引用计数 + 不朽对象"绕过了这个瓶颈。
Q:子解释器(PEP 734)和自由线程是一回事吗?
A:★不是★,是两条独立的路线:
子解释器:一个进程里跑多个解释器,★每个有自己的 GIL★(per-interpreter GIL),
对象★不共享★(要通过 channel 传递)→ 更像"轻量级进程"
自由线程:一个解释器、多个线程、★没有 GIL★、对象直接共享
→ 两者可以并存,适用场景不同(隔离性 vs 共享效率)
Q:我该现在就把多进程代码改成多线程吗?
A:★不该★。等 3.14+ 稳定、依赖链适配完成、并且你的场景确实受益于
"共享内存零拷贝"时再考虑;而且多进程的★故障隔离★仍是它独有的优势。
最后澄清几个高频疑问。GIL 是 CPython 的实现特性而不是 Python 语言特性(Jython、IronPython 从来没有 GIL)——面试时说「Python 有 GIL」不够严谨。GIL 也不意味着多线程完全没用:执行 IO 系统调用、time.sleep、以及 numpy 的大矩阵运算时 GIL 都会被释放,这正是「IO 用线程、CPU 用进程」这条经验的由来。无 GIL 之后 asyncio 依然有价值——它解决的是「海量并发连接」(10 万连接不可能开 10 万线程),和无 GIL 是互补关系。还要分清子解释器(PEP 734)和自由线程是两条独立路线:子解释器是「一个进程多个解释器、每个有自己的 GIL、对象不共享」(更像轻量级进程),自由线程是「一个解释器多个线程、没有 GIL、对象直接共享」——前者胜在隔离性,后者胜在共享效率。
记忆钩子:「★『Python 3.13 移除了 GIL』这个说法不准确★——它提供的是一个★可选的独立构建版本 python3.13t★(带 t 后缀,可与普通 python3.13 共存),3.13 里是实验性、3.14 转为官方支持但★仍非默认★。移除 GIL 难在★引用计数★:每个对象的 ob_refcnt 每秒要增减上亿次,用原子操作或互斥锁保护会让单线程崩塌(1999 和 2015 的两次尝试都栽在这里)。PEP 703 靠四招破局:★偏向引用计数★(对象记住拥有者线程,拥有者用非原子操作、跨线程才用原子操作)、★不朽对象★(None/True/小整数永不计数)、延迟引用计数、dict/list 的细粒度锁 + 无锁读,外加换 mimalloc 分配器。代价是★单线程慢 5%
10%★(3.14 已接近持平)和 ★C 扩展必须适配——未适配的扩展被 import 会让解释器自动重新打开 GIL★(用 sys._is_gil_enabled() 查运行时状态,PYTHON_GIL=1 可强制开启,是排查竞态的标准手段)。★对业务代码最重要的一句:没了 GIL 不是不用加锁,而是过去被 GIL 掩盖的竞态会真正暴露★——GIL 从来不保证『一条 Python 语句原子』(counter += 1 是 LOAD/ADD/STORE 三条字节码),只是切换窗口小、出错概率低;无 GIL 后 4 线程各加 100 万的实测结果常在 150 万300 万之间。所以一律用 Lock 或 Queue,别去背『哪些操作碰巧原子』。选型上:★无 GIL 多线程的杀手锏是共享内存零拷贝★(多进程要 pickle 或 shared_memory),但多进程的★故障隔离★和 asyncio 的★海量连接★优势都还在。最后:★GIL 是 CPython 的实现特性不是语言特性★;子解释器(PEP 734,每解释器一个 GIL、对象不共享)和自由线程是两条不同的路线。」
七、常见误区与追问
- 误区:Python 3.13 已经移除了 GIL,升级上去就能享受多核。 3.13 提供的是一个可选的、独立编译的构建版本——可执行文件叫
python3.13t(带t后缀),需要单独安装(pyenv install 3.13.0t、官方安装器里勾选、或./configure --disable-gil自行编译);你通过常规渠道装的python3.13仍然带 GIL,行为和 3.12 没有区别。而且 3.13 里自由线程是实验性的,3.14 才转为「官方支持但仍非默认」,PEP 703 明确规划了多年过渡期,如果生态适配不理想甚至可能回退。所以正确的表述是「3.13 引入了可选的自由线程构建」,而不是「Python 移除了 GIL」。 - 误区:没有 GIL 之后,多线程代码需要加更多锁;有 GIL 时不加锁也是安全的。 有 GIL 时也不安全——GIL 只保证「同一时刻只有一个线程执行字节码」,从不保证一条 Python 语句是原子的:
counter += 1编译成LOAD/ADD/STORE三条字节码,线程随时可能在中间被切走(默认每 5ms 或遇到阻塞调用)。所以竞态一直存在,只是切换点少、窗口小,表现为「跑一万次才错一次」这种极难复现的 bug。移除 GIL 只是把出错概率从百万分之一提高到几乎必然(实测 4 线程各加 100 万,结果常在 150 万~300 万)。结论不是「无 GIL 需要更多锁」,而是「你本来就该加锁,只是过去侥幸没出事」——所有共享可变状态都应该用Lock或改用Queue传递。 - 误区:装上自由线程版本,我的程序就能跑在无 GIL 模式下了。 未必——如果程序导入了没有声明支持自由线程的 C 扩展,解释器会自动重新启用 GIL 并发出
RuntimeWarning(提示某个模块未声明可安全运行于无 GIL 环境)。也就是说,只要依赖链里有一个未适配的扩展,整个进程就退回到有 GIL 的状态,性能收益归零。判断方法是sys._is_gil_enabled()(运行时 GIL 是否启用)而不只是sysconfig.get_config_var("Py_GIL_DISABLED")(构建是否支持)。目前numpy 2.1+、Cython 3.1+、pillow、cryptography等已适配,但大型项目(如 pandas、pytorch)还在过渡——真正享受收益的前提是整条依赖链都完成适配。 - 误区:自由线程构建就是全面的性能提升。 单线程程序反而更慢——3.13 上大约慢 5%~10%(早期版本更多,3.14 已优化到接近持平)。原因是偏向引用计数、细粒度锁、更大的对象头都有开销,只是被设计得尽量小。收益只在「多线程 + CPU 密集 + 依赖链全适配」三个条件同时满足时才出现;纯 IO 密集的程序在有 GIL 时多线程本来就有效(IO 期间 GIL 会释放),换到无 GIL 提升有限;而单线程脚本、Web 请求处理这类场景可能净亏。另外别忘了开发成本:真实的并发正确性问题会开始暴露,测试和调试的难度都上升。
- 误区:子解释器(PEP 734)和自由线程是同一件事的两种说法。 是两条独立的技术路线,解决同一个问题的不同侧面。子解释器:一个进程里创建多个相互独立的解释器实例,每个解释器有自己的 GIL(per-interpreter GIL,PEP 684),因此可以真正并行;但它们的对象不共享——数据要通过 channel/队列在解释器之间传递(3.13 起有
interpreters模块的实验性支持),本质上更像「比进程轻、比线程隔离」的中间形态。自由线程:只有一个解释器,多个线程,没有 GIL,对象直接共享。取舍很清晰:子解释器胜在隔离性(一个解释器出问题不影响别的、全局状态互不干扰),自由线程胜在共享效率(零拷贝)。两者可以并存,未来可能形成「用子解释器做隔离单元、用自由线程做单元内并行」的组合。 - 追问:为什么移除 GIL 这么难,Java 就没有这个问题? 根本差异在内存管理方式。Java 用垃圾回收(GC),对象的存活由 GC 追踪,普通的字段读写不需要维护任何计数器,所以多线程访问对象天然不涉及「每次访问都要改一个共享计数」的问题。CPython 用引用计数:每个对象有
ob_refcnt,赋值、传参、返回、离开作用域都要增减它,频率高达每秒上亿次。多线程同时增减同一个计数就是竞态——计数错误会导致对象被提前释放(崩溃)或永不释放(泄漏)。如果给每次计数操作加原子指令或锁,单线程性能会大幅下降(历史上 Greg Stein 1999 年的补丁让单线程慢一倍,Larry Hastings 的 Gilectomy 也卡在同一处)。PEP 703 的突破正是绕开这个瓶颈:偏向引用计数让「拥有者线程」用非原子操作(覆盖了绝大多数情况),不朽对象让最热门的None/True/小整数完全不参与计数。 - 追问:GIL 存在的时候,多线程为什么对 IO 密集型任务仍然有效? 因为 GIL 在阻塞期间会被主动释放。CPython 的 IO 相关实现遵循固定模式:进入
read/write/recv/accept等系统调用之前释放 GIL、返回后重新获取。所以当一个线程阻塞在网络或磁盘 IO 上时,GIL 是空闲的,其他线程可以正常执行 Python 字节码——这就是「IO 密集用线程、CPU 密集用进程」这条经验的底层原因。同样会释放 GIL 的还有time.sleep()、锁的等待,以及部分 C 扩展的密集计算(numpy做大矩阵运算时会释放 GIL,所以 numpy 计算的多线程其实一直能并行)。反过来,纯 Python 的 CPU 计算全程持有 GIL,多线程只会因为每 5ms 一次的强制切换(sys.setswitchinterval())而互相争抢,不但没有加速还会因为切换开销而变慢。 - 追问:现在的项目应该为自由线程做哪些准备? 三个层次。① 业务代码(现在就该做,且与 GIL 无关):所有共享可变状态用
Lock保护或改用Queue传递;消除模块级可变全局状态(改成显式传参、依赖注入或threading.local);避免「检查后修改」(if k not in d: d[k] = v)这类非原子模式;用concurrent.futures而不是手搓线程管理。② 测试:在 CI 里加一个跑在python3.13t上的任务(很多隐藏竞态会直接暴露);有 GIL 时也可以用sys.setswitchinterval(0.000001)提高切换频率来放大竞态;写压力测试断言「最终一致性」。③ 库作者:纯 Python 库要审查缓存、单例、延迟初始化的线程安全性;C 扩展要声明Py_MOD_GIL_NOT_USED、审查模块级全局状态、必要时改用原子操作,Cython 项目升级到 3.1+ 并开启freethreading_compatible,并发布带cp313t标签的 wheel。生产环境切换本身不着急——等 3.14+ 稳定且依赖链适配完成再评估。
八、加强记忆
「Python 3.13 移除了 GIL」这个说法不准确——它提供的是一个可选的、独立编译的构建版本 python3.13t(带 t 后缀,可与普通 python3.13 共存),3.13 里是实验性,3.14 转为官方支持但仍非默认。移除 GIL 之所以难,难在引用计数:每个对象的 ob_refcnt 每秒要增减上亿次,用原子操作或互斥锁保护会让单线程性能崩塌(1999 年 Greg Stein 的补丁和 2015 年的 Gilectomy 都栽在这里;Java 没有这个问题是因为它用 GC 而不是引用计数)。PEP 703 靠四招破局:偏向引用计数(对象记住拥有者线程,拥有者用非原子操作、跨线程才用原子操作)、不朽对象(None/True/小整数永不计数)、延迟引用计数、dict/list 的细粒度锁 + 无锁读,外加换用 mimalloc 分配器。代价是单线程慢 5%~10%(3.14 已接近持平)和 C 扩展必须适配——未适配的扩展一旦被 import,解释器会自动重新打开 GIL(用 sys._is_gil_enabled() 查运行时状态,PYTHON_GIL=1 可强制开启,这是排查竞态的标准手段)。对业务代码最重要的一句:没了 GIL 不是「需要加更多锁」,而是「过去被 GIL 掩盖的竞态会真正暴露」——GIL 从来不保证「一条 Python 语句是原子的」(counter += 1 是 LOAD/ADD/STORE 三条字节码),只是切换窗口小、出错概率低;无 GIL 后 4 线程各加 100 万的实测结果常在 150 万~300 万之间。所以一律用 Lock 或 Queue,别去背「哪些操作碰巧原子」。选型上:无 GIL 多线程的杀手锏是共享内存零拷贝(多进程要 pickle 或 shared_memory),但多进程的故障隔离和 asyncio 的海量连接优势依然存在,三者互补。最后两点:GIL 是 CPython 的实现特性而非 Python 语言特性(Jython/IronPython 从来没有);子解释器(PEP 734,每解释器一个 GIL、对象不共享、胜在隔离)和自由线程(一个解释器、无 GIL、对象共享、胜在效率)是两条不同的路线。