Python 3.14 无 GIL 深度实战:当 CPython 终于解开全局锁,多线程真能起飞吗?
三十年里,Python 程序员听过最多的嘲讽就是一句话:"Python 的多线程是假的。"这句话的罪魁祸首只有一个——GIL(Global Interpreter Lock,全局解释器锁)。2025 年 10 月,Python 3.14 把"无 GIL 构建"从实验室推到了"可用于生产"的门槛;到 2026 年,越来越多团队开始认真评估:这把锁,到底是不是真的能摘掉了?本文不堆名词,带你从 GIL 的黑历史、free-threaded 编译、线程安全陷阱,一路打到生产迁移的完整工程手册。
一、背景介绍:GIL 这把锁,锁了 Python 三十年
1.1 多线程为什么在 Python 里"假"
先说清楚一个最常见的误解:GIL 不是一把锁住所有资源的锁,它只锁住"同一时刻只有一个线程能执行 Python 字节码"这件事。
CPython 的解释器内部有大量"全局可变状态":对象的引用计数 ob_refcnt、小整数池、导入系统、垃圾回收器、字典的内部结构……这些状态如果不是线程安全的,两个线程同时改就会发生灾难。最简单的保护手段,就是"同一时刻只允许一个线程跑字节码",于是 GIL 诞生了。
后果是什么?在多核 CPU 上,你开 8 个线程做 CPU 密集计算,操作系统确实把 8 个线程调度到 8 个核上,但 GIL 让它们排队进解释器——任意时刻只有一个在跑,其余都在等锁。结果就是:CPU 密集的多线程,在 CPython 里几乎永远只能用一个核。
import threading, time
def burn(n):
x = 0
for i in range(n):
x += i * i
return x
# 单线程
t0 = time.perf_counter()
burn(10_000_000)
print("single:", time.perf_counter() - t0)
# 两个线程
t0 = time.perf_counter()
a = threading.Thread(target=burn, args=(5_000_000,))
b = threading.Thread(target=burn, args=(5_000_000,))
a.start(); b.start(); a.join(); b.join()
print("double:", time.perf_counter() - t0)
# 带 GIL:double 几乎不比 single 快,甚至可能更慢(锁切换开销)
这就是为什么 Python 搞并发有三套路子:多线程(IO 密集友好,因为等待 IO 时会释放 GIL)、multiprocessing(真正多进程多核,但IPC 成本高)、asyncio(单线程协作式,IO 密集利器)。CPU 密集只能靠多进程或把热点丢给 C 扩展(numpy 的矩阵运算之所以快,正是因为运算时释放了 GIL)。
1.2 为什么不能"简单删掉" GIL
很多人拍脑袋:"把那把锁去掉不就行了?"但 GIL 不是孤立的一行 lock.acquire(),它是 CPython 内存模型的基石假设:
- 引用计数是非原子的
ob_refcnt++,去掉 GIL 后两个线程同时加减,对象会被提前释放或内存泄漏。 - 很多 C 扩展(numpy、pandas、lxml……)在写的时候默认了 GIL 提供的串行保护,直接去 GIL 会让这些库在并发下崩溃。
- 解释器内部几乎所有的全局表(import 缓存、类型字典、GC 跟踪列表)都假设单线程访问。
所以"去 GIL"本质上是一场对 CPython 内部数据结构的线程安全化改造,工作量巨大。这也是为什么它从 Python 2 时代就被讨论,一直到 3.13/3.14 才真正落地。
1.3 PEP 703 与 free-threaded 的由来
2023 年,PEP 703(Making the Global Interpreter Lock Optional)被接受。它的核心思路不是"删掉 GIL",而是"让 GIL 变成可选项":通过编译开关 --disable-gil,生成一个没有 GIL 的解释器二进制。
关键设计决策:
- 不破坏现有生态:默认构建仍然带 GIL,所有现有代码、C 扩展照常工作。
- 并行推进:用原子操作、细粒度锁、无锁数据结构,把解释器内部状态逐步线程安全化。
- 兼容层:为还没迁移的 C 扩展提供"模拟 GIL"的回退机制,避免一上来就全崩。
Python 3.13(2024-10)首次以实验形态提供 no-GIL 构建,二进制名字带 t 后缀(如 python3.13t,t = threading)。到 Python 3.14(2025-10),free-threaded 构建被推进到"可用于生产"的阶段:引用计数原子化、关键全局状态加锁、性能回退被显著压缩。但截至 3.14,默认构建依然带 GIL,无 GIL 需要显式安装 python3.14t 或自行 --disable-gil 编译。
一句话总结背景:GIL 是历史包袱,不是设计缺陷;去 GIL 是给 CPython 做一场全身麻醉下的心脏手术,而 3.14 是这场手术第一次让病人能下床走路的版本。
二、核心概念:free-threaded 到底改了什么
2.1 怎么确认自己在"无 GIL"的解释器里
最直观的自检方式:
import sys
print("version:", sys.version)
print("GIL enabled:", sys._is_gil_enabled())
# 带 GIL 的常规构建 -> True
# free-threaded 构建 -> False
sys._is_gil_enabled() 返回 False 就说明你跑在 no-GIL 解释器里。注意这是下划线开头的私有 API,它的存在本身就是"这个功能还在演进中"的信号——生产代码不要依赖它做分支逻辑,只用来诊断。
如果你用的是 pyenv:
pyenv install 3.14.0t # 注意带 t 后缀的版本
pyenv local 3.14.0t
python -c "import sys; print(sys._is_gil_enabled())" # False
conda 用户则是:
conda create -n py314t python=3.14 numpy pandas pytest free-threading
conda activate py314t
2.2 GIL 锁的到底是不是"内存"
一个必须澄清的点:GIL 不保护你的数据,它保护的是解释器。 即使在 free-threaded 模式,你的 Python 对象(list、dict、自定义类实例)依然不是线程安全的。这一点和 Java 很像——语言层面去掉了全局锁,但共享可变状态照样要你自己加锁。
很多人以为"无 GIL = 随便并发",这是最大的坑。下面第三节会用一个血淋淋的 race condition 演示。
2.3 引用计数原子化:去 GIL 的第一块砖
CPython 用引用计数做主要的内存管理。Py_INCREF / Py_DECREF 原本是普通的整数加减:
// 旧实现(带 GIL 时安全)
#define Py_INCREF(op) ((op)->ob_refcnt++)
// free-threaded 实现(原子)
#define Py_INCREF(op) _Py_atomic_add_int_relaxed(&(op)->ob_refcnt, 1)
把 ob_refcnt 的加减改成原子操作(C11 atomic_int 或平台原子指令),是第一块、也是最关键的一块砖。这样两个线程同时持有/释放同一个对象,计数不会错乱,对象不会被误回收。
但原子操作比普通加法慢。这就是 free-threaded 模式单线程性能会有回退的根本原因(实测通常在 5%~20% 之间,取决于引用计数操作的热度)。代价换来的,是多线程真正并行执行 Python 字节码的可能。
2.4 线程安全模型的反转
带 GIL 时,你的心智模型是:
"我写的这段代码,在没有
time.sleep/IO 的情况下,是原子执行的,别的线程插不进来。"
free-threaded 时,这个假设彻底反转:
"任何对共享可变状态的访问,如果没有显式同步,就可能在任意字节码边界被另一个线程打断。"
这意味着:所有 list.append、dict[k]=v、x += 1、对实例属性的读写,在并发场景下都要重新审视。好消息是——queue.Queue、concurrent.futures、大部分标准库线程安全原语本身就有锁,可以放心用。
2.5 子解释器(PEP 554)与 free-threading 的关系
这是很多人混淆的两个概念:
- free-threaded:一个解释器内部,多个线程可以真正并行。
- 子解释器(PEP 554,3.12+ 稳定,3.13+ 的
interpreters模块):一个进程里跑多个互相隔离的解释器,每个有自己的 GIL、自己的模块拷贝。它们天然并行,且不需要 free-threaded 构建。
两者是正交的。你可以:
- 用 free-threaded 做"同一份数据、多线程并行处理";
- 用子解释器做"完全隔离的任务沙箱"(比如插件系统,每个插件跑在自己解释器里,崩了一个不影响别人);
- 甚至两者结合:free-threaded 的解释器里再起子解释器。
三、架构分析:CPython 内部为去 GIL 做了哪些手术
3.1 _PyMutex:比一把大锁聪明得多
如果给每个对象都加一把 threading.Lock(基于操作系统互斥量),开销会爆表——光是创建几百万个小对象就先被锁拖死。CPython 引入了一个内部低开销互斥锁 _PyMutex:
- 无竞争时几乎零成本:用一个原子标志位自旋,绝大多数访问是单线程的,直接过。
- 轻度竞争时短暂自旋:几个微秒级别的自旋,避免立刻陷入内核态。
- 重度竞争时才陷入 OS 等待:让出 CPU,不空转烧核。
这套"用户态快速路径 + 内核态慢速路径"的设计,和 Rust 的 std::sync::Mutex、Go 的 sync.Mutex 思路一致。正是它,让"给海量对象加锁"在绝大多数单线程场景下几乎无感。
3.2 哪些全局状态被重新上了锁
CPython 团队逐一审计了内部状态,对以下热点做了线程安全化:
| 内部状态 | 改造方式 |
|---|---|
ob_refcnt 引用计数 | 原子操作 |
| 小整数池 / 字符串驻留表 | _PyMutex 保护 |
导入系统 sys.modules | 细粒度锁 |
| 垃圾回收器跟踪列表 | 原子结构 + 锁 |
| 字典 split-table(类型属性共享) | 改为 per-interpreter 或加锁 |
| 类型对象缓存 | 原子化 |
| 内置单例(None/True/False) | 保持不可变,无需锁 |
注意一个细节:字典的 split-table 优化被弱化了。带 GIL 时,同一类的所有实例共享一张"键表"(split table),省内存又快。无 GIL 下共享表要加锁,反而可能更慢,所以 free-threaded 构建里实例字典更偏向独立存储。这对"海量小对象"场景的内存/性能画像会有微妙变化。
3.3 内存模型与 happens-before
带 GIL 时,"单线程执行"天然提供了 happens-before 关系,一个线程写的值,另一个线程一定能读到最新值。
free-threaded 去掉了这层保证。现在的内存可见性遵循原子操作 / 锁的 happens-before 规则:
- 通过
threading.Lock保护的写,释放锁后对加锁的读可见。 - 通过
queue.Queue.put/get传递的数据,天然可见(队列内部有锁)。 - 裸写裸读(无同步)的可见性不保证,可能读到陈旧值——这正是 race condition 的来源。
理解这一点,你就不会再幻想"我多写几次总会读到"。并发正确性只能靠同步原语建立 happens-before,不能靠运气。
3.4 代价:单线程到底回退多少
这是工程决策最关心的数字。综合社区与官方基准:
- 单线程纯 Python 计算:回退约 5%~15%(引用计数原子化 + 字典去 split-table)。
- 单线程 IO 密集:几乎无感,瓶颈在 IO 不在锁。
- 引入大量 C 扩展且未做 free-threaded 适配:可能回退更多,甚至触发"模拟 GIL"导致又变串行了。
所以结论很明确:free-threaded 不是"免费升级",它是用单线程的一点点代价,换取多线程 CPU 密集任务的线性扩展能力。 你的负载如果本来就是 IO 密集或单线程足够快,没必要换。
3.5 free-threaded 与 multiprocessing / asyncio 的边界
三者定位不冲突,反而互补:
- asyncio:单线程协作式,IO 密集、超高并发连接的王者,和 free-threaded 无关。
- multiprocessing:跨进程、绕过 GIL、吃满多核,但 IPC(队列/管道/共享内存)成本高、调试难。适合"粗粒度"并行任务。
- free-threaded 多线程:同一进程内共享内存、低开销并行,适合"细粒度、数据共享频繁"的 CPU 密集任务(比如对一个大数组分块并行计算,各块共享同一块内存)。
选型口诀:IO 多 → asyncio;任务重且独立 → multiprocessing;任务重且数据要共享 → free-threaded 多线程。
四、代码实战:从检测到迁移的完整手册
4.1 实战一:环境自检脚本
把它做成 CI 里的一行断言,防止误用带 GIL 的解释器跑并行代码:
# check_nogil.py
import sys
def require_free_threaded():
if sys._is_gil_enabled():
raise RuntimeError(
"本程序需要 free-threaded 解释器(python3.14t),"
"当前运行在带 GIL 的构建上,多线程将无法并行。"
)
if __name__ == "__main__":
try:
require_free_threaded()
print("OK: 运行在 free-threaded 解释器")
except RuntimeError as e:
print("WARN:", e)
4.2 实战二:CPU 密集多线程基准(有锁 vs 无锁)
这是最能说明问题的实验。我们用纯 Python 的 CPU 密集函数,对比单线程 vs 8 线程,分别在带 GIL 和 free-threaded 下跑。
# bench_cpu.py
import sys, time
from concurrent.futures import ThreadPoolExecutor
def cpu_bound(n: int) -> int:
"""纯 Python CPU 负载:模拟数值计算"""
total = 0
for i in range(n):
total = (total + (i * i) % 97) & 0xFFFF
total ^= (i << 1) & 0xFF
return total
def run_single(count, per):
return sum(cpu_bound(per) for _ in range(count))
def run_multi(count, per, workers):
with ThreadPoolExecutor(max_workers=workers) as ex:
futs = [ex.submit(cpu_bound, per) for _ in range(count)]
return sum(f.result() for f in futs)
if __name__ == "__main__":
COUNT, PER, WORKERS = 8, 3_000_000, 8
print(f"GIL enabled: {sys._is_gil_enabled()} | cores: {WORKERS}")
t0 = time.perf_counter(); run_single(COUNT, PER); t1 = time.perf_counter()
print(f"[single ] {t1 - t0:6.2f}s")
t2 = time.perf_counter(); run_multi(COUNT, PER, WORKERS); t3 = time.perf_counter()
print(f"[multi x8 ] {t3 - t2:6.2f}s")
print(f"[speedup ] {(t1 - t0) / (t3 - t2):.2f}x")
预期结果(8 核机器):
| 构建 | 单线程 | 8 线程 | 加速比 |
|---|---|---|---|
| 带 GIL | ~6.0s | ~6.2s | ~0.97x(基本没用) |
| free-threaded | ~6.8s | ~1.1s | ~6.2x(接近线性) |
看到没?同样的代码,换一个解释器二进制,CPU 密集多线程从"完全没用"变成"接近线性扩展"。这就是去 GIL 的全部意义。注意 free-threaded 的单线程那栏略慢(6.8s vs 6.0s),就是第三节说的原子引用计数代价。
4.3 实战三:共享可变状态的 race condition 与修复
现在上真正的坑。下面这段代码在 free-threaded 下必然出错:
# race_bad.py
from threading import Thread
counter = 0
def incr():
global counter
for _ in range(1_000_000):
counter += 1 # 读作 a=counter; a+=1; counter=a —— 三步非原子
ts = [Thread(target=incr) for _ in range(4)]
for t in ts: t.start()
for t in ts: t.join()
print("expected:", 4_000_000)
print("got :", counter) # 大概率远小于 4_000_000
为什么?counter += 1 编译成字节码是 LOAD → BINARY_ADD → STORE 三步。无 GIL 时,线程 A 刚 LOAD 完旧值,线程 B 也 LOAD 了同一个旧值,两者都 +1 存回,结果只 +1 而不是 +2。四线程各跑一百万次,丢失的更新数以十万计。
修复方式一:用 threading.Lock
# race_fixed_lock.py
from threading import Thread, Lock
counter = 0
lock = Lock()
def incr():
global counter
for _ in range(1_000_000):
with lock:
counter += 1
ts = [Thread(target=incr) for _ in range(4)]
for t in ts: t.start()
for t in ts: t.join()
print("got:", counter) # 稳定等于 4_000_000
修复方式二:用 queue.Queue 把"共享状态"换成"消息传递"
# race_fixed_queue.py
from threading import Thread
from queue import Queue
q = Queue()
def worker():
local = 0
while True:
item = q.get()
if item is None:
q.task_done()
break
local += item
q.task_done()
results.append(local)
results = []
ts = [Thread(target=worker) for _ in range(4)]
for t in ts: t.start()
for _ in range(4_000_000):
q.put(1)
for _ in range(4):
q.put(None) # 毒丸,通知退出
q.join()
print("got:", sum(results)) # 4_000_000
经验法则:能用消息传递(Queue)就别用共享内存 + 锁。消息传递把并发正确性收敛到队列这一处,远比到处加锁好维护。
4.4 实战四:queue.Queue 生产者-消费者流水线
真实场景里,free-threaded 最香的用法是"一条流水线,多个 worker 真的并行处理 CPU 密集任务":
# pipeline.py
import time
from concurrent.futures import ThreadPoolExecutor
from queue import Queue
def transform(x: int) -> int:
# 假设这是一个 CPU 密集的转换
return sum((x * i) % 97 for i in range(5000))
def main():
in_q: Queue = Queue(maxsize=64)
out_q: Queue = Queue(maxsize=64)
def producer():
for i in range(2000):
in_q.put(i)
for _ in range(8):
in_q.put(None) # 8 个毒丸,对应 8 个 worker
def worker():
while True:
item = in_q.get()
if item is None:
in_q.task_done()
break
out_q.put(transform(item))
in_q.task_done()
def consumer():
done = 0
while done < 2000:
r = out_q.get()
done += 1
out_q.task_done()
print("consumed", done, "results")
t0 = time.perf_counter()
tp = ThreadPoolExecutor(max_workers=1)
tw = ThreadPoolExecutor(max_workers=8)
tc = ThreadPoolExecutor(max_workers=1)
tp.submit(producer)
for _ in range(8):
tw.submit(worker)
tc.submit(consumer)
tp.shutdown(); tw.shutdown(); tc.shutdown()
print(f"elapsed: {time.perf_counter() - t0:.2f}s")
if __name__ == "__main__":
main()
在 free-threaded 解释器里,transform 的 8 个 worker 真正并行吃满 8 核;换成带 GIL 的构建,这 8 个 worker 又退化成排队执行——流水线吞吐直接腰斩。
4.5 实战五:C 扩展迁移的真相(numpy / pandas / pybind11)
这是 free-threaded 落地的最大拦路虎。Python 对象层面去 GIL 了,但你的 numpy、pandas、torch 还是 C 写的,它们内部假设了 GIL 的串行保护。
现状(截至 3.14):
- 主流科学计算栈(numpy、scipy、pandas、matplotlib)已经陆续发布 free-threaded 专用 wheel,但需要在
python3.14t环境下用pip install装对应的版本,不能用普通环境的包。 - 纯 Python 写的库(绝大多数 Web 框架、HTTP 客户端)基本开箱即用。
- 自己写的 C 扩展(ctypes 调用 C 动态库除外)需要重新评估:
- 如果用了 Limited API / 稳定 ABI 且只通过官方宏操作对象,迁移成本较低;
- 如果直接操作
PyObject*、假设 GIL 串行、或在无锁情况下读写全局 C 变量,则需要加锁或改用PyUnstable_前缀的线程安全 API。
- pybind11 / Cython:新版已支持 free-threaded,但生成的代码里涉及全局状态的部分仍要你手动加锁。
一个最小验证脚本,确认你的数据科学栈真的在 no-GIL 下工作:
# check_stack.py
import sys, numpy as np
print("GIL:", sys._is_gil_enabled()) # False
a = np.random.rand(20_000_000)
# 用多线程并行做矩阵运算——注意 numpy 自身释放 GIL,
# 但 Python 层循环调度若想并行仍需 free-threaded
from concurrent.futures import ThreadPoolExecutor
def chunk_mean(lo, hi):
return float(a[lo:hi].mean())
N = 8
size = len(a) // N
with ThreadPoolExecutor(max_workers=N) as ex:
means = list(ex.map(lambda i: chunk_mean(i*size, (i+1)*size), range(N)))
print("means:", means)
现实建议:如果你的生产负载依赖未适配 free-threaded 的 C 扩展,先别急着上 3.14t。等生态 wheel 齐全,或者把热点用 Rust 扩展(通过 PyO3,新版同样支持 no-GIL)重写。
4.6 实战六:子解释器做"真·并行沙箱"
即便你不换 free-threaded 构建,Python 3.13+ 的 interpreters 模块也能让你在一个进程里跑多个隔离解释器,每个自带 GIL、天然并行:
# subinterp.py
import interpreters
import textwrap
main = interpreters.main()
print("main id:", main)
# 创建子解释器
child = interpreters.create()
print("child id:", child)
src = textwrap.dedent("""
import sys, time
print("child running, GIL enabled:", sys._is_gil_enabled(), "pid-safe sandbox")
result = sum(i*i for i in range(1_000_000))
""")
interpreters.run_string(child, src)
# 通过 channel 传数据(隔离内存,只能显式通信)
interpreters.channel_create() # 简化示意
interpreters.destroy(child)
print("done")
子解释器适合插件系统、不可信代码沙箱、需要强隔离的并行任务——它比多线程隔离性强,比多进程内存占用低。和 free-threaded 组合,你能同时拥有"进程内隔离"和"线程内并行"两种能力。
五、性能优化:什么时候真快,什么时候反而慢
5.1 加速成立的三个前提
free-threaded 的多线程加速,只有在以下条件同时满足时才成立:
- 任务确实是 CPU 密集(不是 IO 等待)。IO 密集老老实实用 asyncio,无 GIL 帮不了你。
- 任务是可并行的(各子任务之间独立,或只通过 Queue/锁做少量同步)。有强数据依赖(A 的结果必须给 B)的任务无法并行。
- 你的依赖链已经 free-threaded 化。只要关键路径上有一个没适配的 C 扩展触发"模拟 GIL",整条链路又变串行。
5.2 反模式:锁争用与退化
最常见的性能陷阱是锁粒度太粗:
# 反例:一把大锁把所有并行都堵死了
lock = Lock()
shared = []
def worker(items):
for it in items:
with lock: # 整个循环都锁住 -> 等于串行
shared.append(process(it))
正确做法是缩小临界区,只锁真正竞争的那一行:
# 正例:先计算,只在写入时加锁
def worker(items):
local = [process(it) for it in items] # 无锁并行计算
with lock:
shared.extend(local) # 只在合并时加锁
另一个陷阱是伪共享(false sharing):多个线程反复写同一缓存行的不同变量,导致 CPU 缓存行互相失效,性能暴跌。解决方法是让每个线程操作自己独立的数据块,最后再合并——这正是 4.4 流水线示例的做法。
5.3 混合架构:free-threaded + asyncio + Rust 扩展
工业级的正确姿势是分层:
┌─────────────────────────────────────────┐
│ asyncio 事件循环:扛 10w+ 并发连接(IO) │
├─────────────────────────────────────────┤
│ free-threaded 线程池:并行做 CPU 密集计算 │
├─────────────────────────────────────────┤
│ Rust/PyO3 扩展:热点算法,释放 GIL 跑满 │
└─────────────────────────────────────────┘
- 用 asyncio 接请求(IO 密集,单线程足够)。
- 把需要计算的请求丢进
run_in_executor(背后是 free-threaded 线程池),真正并行。 - 热点算法用 Rust 写(PyO3 支持 no-GIL),在 C 层释放 GIL 跑满多核。
这样每一层都用对了并发模型,而不是指望一种机制包打天下。
5.4 调试工具链
free-threaded 的 bug 是非确定性的,比普通 bug 难调一个数量级。武器库:
threading模块 + 日志:在每个临界区入口打印线程 id,复现竞争窗口。- py-spy:
py-spy dump --pid <pid>看各线程在干什么,free-threaded 下能看到多个线程真在并行跑(带 GIL 下它们会"挤"在同一帧)。 - ThreadSanitizer(C 扩展层):给 CPython 和你的 C 扩展编译时加
-fsanitize=thread,能抓到 C 层的 data race。 faulthandler:import faulthandler; faulthandler.enable(),崩溃时打印 Python 回溯,定位是哪个线程炸的。- 压力测试 + 重复跑:race condition 不是每次都现,要在多核机器上循环跑上千次验证修复有效性。
六、总结展望:3.14 该不该上,怎么上
6.1 给程序员的决策框架
问自己三个问题:
- 我的负载是 CPU 密集且可并行吗? 否 → 别上,asyncio/多进程够用。
- 我的依赖链有 free-threaded wheel 吗? 没有关键 C 扩展适配 → 暂缓,等生态。
- 我愿意为单线程 ~10% 回退买单吗? 如果是延迟敏感的单线程服务 → 谨慎。
三者都通过,再上 python3.14t。
6.2 迁移落地清单
- 用
python3.14t/ condafree-threading环境搭建 - CI 加
check_nogil.py断言,防止误用带 GIL 构建 - 审计所有"共享可变状态",把裸读写换成
Lock或Queue - 重装依赖,确认装到的是 free-threaded 专用 wheel
- 用基准脚本(4.2/4.4)验证多线程确实加速
- 上线前用 ThreadSanitizer + 多核压力测试跑崩溃复现
6.3 生态时间线
到 2026 年中,科学计算栈(numpy/pandas/scipy)和机器学习框架(torch 等)的 free-threaded 支持已经初具规模,但完全无感迁移尚需时日。社区的判断是:3.14 是"可用",3.15/3.16 才可能是"默认推荐"。CPython 核心团队也在推进"让 free-threaded 成为默认构建"的长期目标,但在此之前,default 仍是带 GIL 的安全构建。
6.4 对 Python 未来的意义
去 GIL 不是一次小优化,而是 Python 在"多核时代"的身份重建。过去二十年,Python 靠"胶水语言"定位——逻辑用 Python 写,性能热点丢给 C 扩展。去 GIL 之后,Python 自己也能写真正的并行计算,胶水层变薄,纯 Python 能干的活变多。
这并不意味着 Rust/Go 会被取代——它们在系统层、内存层的优势无可撼动。但"我有一个 CPU 密集的纯 Python 任务要并行"这件事,从此不再是 Python 的死穴。三十年后,那句"Python 多线程是假的",终于可以收进了。
写在最后:本文所有基准代码都可在 python3.14t 与常规构建下直接对比运行。别只看结论,把 4.2 的脚本跑一遍——当你看到 8 线程从 0.97x 变成 6.2x 的那一刻,会比任何文章都更直观地理解"去 GIL"意味着什么。技术选型没有银弹,但理解锁的本质,能让你在每一次 Thread(start) 时,清楚地知道自己在买什么、卖什么。