← 返回题目列表

Python 3.13 的自由线程(no-GIL)是什么?GIL 真的要没了吗?

困难 第 21 / 27 题 更新于 2026/08/01
自由线程GILPEP 703Python 3.13

简化版

「自由线程(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 扩展必须适配numpypandas 等已陆续支持),未适配的扩展在自由线程版里会自动重新启用 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 += 1list 的检查后修改、字典的 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 这次能成功靠的是四项技术:偏向引用计数(对象记住「拥有者线程」,拥有者用非原子操作改计数,只有跨线程访问才用原子操作——而绝大多数对象只被一个线程碰)、不朽对象NoneTrue、小整数等永不增减计数,消除最热门对象的争抢)、延迟引用计数、以及 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.0tuv 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+pybind11pillowcryptography 等已支持,关键是你的整条依赖链都适配了才能真正享受收益

三、没了 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 万之间)。结论不是「学会哪些操作碰巧原子」,而是「所有共享可变状态一律用 LockQueue 保护」——这本来就是正确写法,只是现在必须做对。

四、和多进程、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+pillowcryptography 等已适配,但大型项目(如 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 万之间。所以一律用 LockQueue,别去背「哪些操作碰巧原子」。选型上:无 GIL 多线程的杀手锏是共享内存零拷贝(多进程要 pickle 或 shared_memory),但多进程的故障隔离asyncio 的海量连接优势依然存在,三者互补。最后两点:GIL 是 CPython 的实现特性而非 Python 语言特性(Jython/IronPython 从来没有);子解释器(PEP 734,每解释器一个 GIL、对象不共享、胜在隔离)和自由线程(一个解释器、无 GIL、对象共享、胜在效率)是两条不同的路线