《Python高级编程》5.2 自由线程构建与迁移影响

先如实交代本机并非自由线程构建,再从 PEP 703 的对象头改造、偏向引用计数、延迟引用计数与 Python 临界区入手,讲清 CPython 如何在拿掉 GIL 后维持内存安全,并给出 C 扩展 Py_mod_gil 迁移与生态评估的完整框架。

本节目标:讲清 PEP 703 如何在不加锁的前提下让 CPython 去掉 GIL,并说明它带来的对象头膨胀、单线程开销与 C 扩展迁移成本。
适用版本:Python 3.12+(实测 3.14.6)

5.2 自由线程构建与迁移影响

上一节我们看到 GIL 把 CPU 密集多线程压成 1.0x。自由线程(free-threading,PEP 703)就是要拆掉这把锁。但拆锁不是删掉一行代码——引用计数本身就不是线程安全的,去掉 GIL 后每一次 ob_refcnt 自增都要重新设计。这一节先如实交代本机环境,再拆 PEP 703 的核心机制。

5.2.1 先看本机:这不是自由线程构建

本节最有价值的「实测」是一条否定结论——必须先把话说清楚,避免读者误以为下面的性能数字来自本机。

import sys, sysconfig

print(sys._is_gil_enabled())                       # True
print(sysconfig.get_config_var("Py_GIL_DISABLED")) # 0
print("free-threading build" in sys.version)       # False
print(sys.version)
# 3.14.6 (main, Jun 10 2026, 10:03:53) [Clang 21.0.0 (clang-2100.0.123.102)]

三个判据一致:_is_gil_enabled() 为 True、Py_GIL_DISABLED 为 0、sys.version 里没有 free-threading build 字样。再试运行期开关,标准构建会直接报错退出:

$ PYTHON_GIL=0 python -c "print('x')"
Fatal Python error: config_read_gil: Disabling the GIL is not supported by this build

$ python -X gil=0 -c "print('x')"
Fatal Python error: config_read_gil: Disabling the GIL is not supported by this build

**因此本节无法给出本机的自由线程性能数字。**下面关于性能的描述全部来自官方文档,我会标注出处;本机验证的部分只有上面这几条「构建类型判据」。判断一台机器能否跑自由线程,标准做法就是下面这组开关:

检查项标准构建(本机)自由线程构建
sysconfig.get_config_var("Py_GIL_DISABLED")01
sys._is_gil_enabled()True默认 False,可用 PYTHON_GIL=1 / -X gil=1 临时开回
sys.version无标记含 free-threading build
可执行文件名python3.14python3.14t(ABI 标签带 t)
构建方式默认./configure --disable-gil && make

如果你要自己拿一个自由线程解释器,官方路径是从源码构建(本机未执行,仅列出流程):

# 从源码构建一个自由线程解释器(官方 --disable-gil 开关)
./configure --disable-gil --with-ensurepip=install
make -j
make altinstall          # 得到 python3.14t,与标准 python3.14 共存

也可以用官方为各平台提供的预编译 free-threaded 安装包(python3.14t)。注意 --disable-gil 构建的 ABI 标签带字母 t,与标准构建是两套 ABI——纯 Python 包通常两边都能装,但 C 扩展必须针对目标 ABI 分别编译,这也是生态迁移慢的根本原因。

5.2.2 对象头改造:一个对象要同时服务多个线程

去掉 GIL 后,引用计数不能再是一把全局锁下的普通整数。PEP 703 的做法是给每个对象加一份自己的同步状态,把对象头从「一个 Py_ssize_t ob_refcnt」扩成一组字段:

typedef struct _object {
    uint32_t      ob_tid;        /* 4B:对象"属主"线程 id */
    uint16_t      __padding;     /* 2B:预留 */
    PyMutex       ob_mutex;      /* 1B:每个对象一把互斥锁 */
    uint8_t       ob_gc_bits;    /* 1B:GC 标志(原来存在 PyGC_Head) */
    uint32_t      ob_ref_local;  /* 4B:属主线程的本地引用计数 */
    Py_ssize_t    ob_ref_shared; /* 共享引用计数 + 状态位 */
    PyTypeObject *ob_type;
} PyObject;

(字段布局引自 PEP 703 规范章节。)三个关键点:

  • 偏向引用计数(biased reference counting):对象有一个「属主线程」。属主自己增减引用只动 ob_ref_local(快路径,无原子操作);其他线程动 ob_ref_shared(慢路径,需要原子操作)。这样单线程程序的引用计数操作几乎不退化。
  • 每个对象一把 PyMutex:不是全局锁,而是细粒度对象锁,只在实际发生竞争时使用。
  • ob_gc_bits 把 GC 标志搬进对象头:因为分代 GC 也依赖 GIL,去锁后必须让 GC 能在对象级别协作。

偏向引用计数的「偏向」体现在读写路径的不对称上。ob_ref_shared 不是单纯的计数,而是把低位当作状态位使用:

状态含义归零时的动作
默认(0b00)纯共享计数直接释放对象
Weakref(0b01)存在弱引用走弱引用清理流程
Queued(0b10)已入合并队列等待队列合并
Merged(0b11)已合并合并完成后再判定

属主线程走快路径(只动 ob_ref_local,不碰原子操作、不碰 ob_mutex);其他线程走慢路径(原子地改 ob_ref_shared)。这套设计的目标很明确:单线程程序的引用计数开销尽量不退化,代价只在真正跨线程共享对象时才付出。

5.2.3 减少锁的巧思:延迟引用计数、永生对象与临界区

只靠「偏向 + 对象锁」还不够——很多高频操作(比如加载模块、调用类方法)会不断触碰全局对象,每次加锁成本太高。PEP 703 叠加了三层优化:

  1. 延迟引用计数(deferred reference counting):对特定类型的对象,用「按对象标志位」开启一种「不即时增减、由 GC 批量处理」的计数模式。官方 howto 明确列出适用类型,包括模块对象、模块顶层函数、类中定义的方法等——都是长期存活、引用变化相对可控的对象。
  2. 永生对象(immortalization):像 None、True、小整数、驻留字符串这类永不释放的对象,直接把引用计数设为「永生」值,彻底跳过增减操作。代价是:自由线程构建下所有 interned 字符串都变成永生,内存只增不减。
  3. Python 临界区(critical sections)与乐观访问:对 dict / list 的常见操作,CPython 采用「乐观」策略——先不加锁直接读,用一个版本号/状态位校验是否被打断,只有冲突时才回退到加锁。这避免了读多写少场景下的锁竞争。

这三层叠加起来,目标只有一个:让「无 GIL」的代价尽量落在多线程竞争上,而不是落在单线程性能上。

5.2.4 无 GIL 不等于不用锁

一个常见误解是「自由线程后多线程就万能了」。恰恰相反,去掉 GIL 后应用层要自己面对的竞态变多了:GIL 曾经顺手保证的「单条字节码原子性」不复存在(回想 5.1.4:裸 counter += 1 在标准构建上零丢失,在自由线程构建上就不再保证)。你需要的 threading.Lock、RLock、Queue 一个都不会少,甚至更需要。

官方 howto 还列出几条已知限制,都是「无 GIL 后仍不安全」的典型:

  • frame 对象:如果某个 frame 正在另一个线程里执行,访问它的 f_locals 可能直接让解释器崩溃。
  • 迭代器:多个线程并发访问同一个迭代器对象不是线程安全的,可能出现重复或遗漏元素。
  • 上下文变量:自由线程构建下 thread_inherit_context 默认为 True,新线程会继承创建者的 context,行为与标准构建不同。

5.2.5 C 扩展迁移:Py_mod_gil 与自动回退

对 C 扩展作者来说,迁移成本集中在一点:你的扩展是否线程安全。CPython 的设计是「默认保守」:

  • 扩展模块通过多阶段初始化声明 Py_mod_gil 槽位,显式告诉解释器「我已经线程安全,可以关 GIL 跑」。
  • 如果导入了一个没有声明该槽位的 C 扩展,解释器会自动把 GIL 重新打开(howto 原文:The GIL may also automatically be enabled when importing a C-API extension module that is not explicitly marked as safe)。

这意味着:一个自由线程构建的 Python,一旦 import 了未适配的 C 扩展,就会悄悄退回 GIL 模式——你的多线程加速可能莫名其妙消失。这也是为什么迁移评估的第一步是盘点依赖树里的 C 扩展(NumPy、Pandas、数据库驱动等)是否已适配。

扩展侧声明「我可以关 GIL 跑」的写法,是在多阶段初始化的模块定义里加一个 Py_mod_gil 槽位:

static PyModuleDef_Slot slots[] = {
    /* 声明本扩展在无 GIL 下线程安全 */
    {Py_mod_gil, Py_MOD_GIL_NOT_USED},
    {0, NULL}
};

static struct PyModuleDef moddef = {
    PyModuleDef_HEAD_INIT, "myext", NULL, 0, methods, slots, NULL, NULL, NULL
};

(上述 C 代码为示意,本机未编译——第 9 章会真编译 C 扩展。)反之,若扩展没有这个槽位,解释器就按「不安全」处理,自动把 GIL 打开。

5.2.6 性能代价与版本路线

单线程性能(来自官方文档,非本机实测):

  • Python 3.14 的 What’s New 表述:自由线程模式下单线程性能损失约为 5%~10%,取决于平台与 C 编译器。
  • pyperformance 基准套件的平均值:macOS aarch64 约 1%,x86-64 Linux 约 8%。
  • 3.14 的一个重要进展:专门化自适应解释器(PEP 659)在自由线程模式下已启用,这是 3.13 到 3.14 性能大幅改善的主要原因。

版本时间线(PEP 703 官方规划):

阶段版本状态
引入 --disable-gil 构建开关,出现两种 ABI3.13已发布(实验性)
完成 PEP 703 实现、C API 收敛、自适应解释器启用3.14已发布(本机 3.14.6 为标准构建)
GIL 由运行期环境变量/开关控制,合并为单一 ABI约 2026–2027规划中
自由线程成为默认约 2028–2030规划中

3.13 到 3.14 之间的自由线程进展值得单独列一下,因为 3.14 是本机基线版本:

维度3.13(首次引入)3.14(本机基线)
PEP 703 实现完成度实验性、有临时绕行方案实现完成,C API 收敛,临时方案被替换
专门化自适应解释器(PEP 659)未启用已启用,性能大幅改善
单线程开销(官方口径)更高约 5%~10%(pyperformance 均值 macOS aarch64 约 1%)
Windows 扩展编译Py_GIL_DISABLED 自动判定需构建后端显式指定

注意时间线是 PEP 703 在 2023 年写下的相对规划(「再 2~3 个版本」),并非承诺的具体版本号。判断某版本是否已默认自由线程,以该版本实际的 Py_GIL_DISABLED 默认值和发布说明为准,不要照搬规划年份。

5.2.7 今天就能用的并行:PEP 734 子解释器(本机实测)

自由线程要等生态,但 3.14 已经把 PEP 734 的 concurrent.interpreters 落地了,而且它在标准构建上就能用——每个子解释器拥有自己的 GIL(PEP 684 的 per-interpreter GIL)。本机实测确认模块可用:

import concurrent.interpreters as iv
print(iv.create())          # Interpreter(1)
print([x for x in dir(iv) if not x.startswith("_")])
# ['ExecutionFailed','Interpreter','InterpreterError','Queue','create','get_main', ...]

关键问题是:每个子解释器一个 GIL,能不能真的并行跑 Python? 让 k 个子解释器各在自己的线程里跑满一份 CPU 计算(N=8_000_000),与「单解释器串行跑 k 份」对比:

import concurrent.interpreters as iv
import threading, time

SRC = "def work(n):\n    s=0\n    for i in range(n): s += i*i\n    return s\n"
N = 8_000_000

def serial(k):                       # 单解释器顺序跑 k 份
    t0 = time.perf_counter()
    for _ in range(k):
        s = 0
        for i in range(N): s += i*i
    return time.perf_counter() - t0

def parallel(k):                     # k 个子解释器 + k 个线程
    interps = []
    for _ in range(k):
        it = iv.create(); it.exec(SRC); interps.append(it)
    ths = []
    def target(it): it.exec(f"__res = work({N})")
    t0 = time.perf_counter()
    for it in interps:
        th = threading.Thread(target=target, args=(it,)); th.start(); ths.append(th)
    for th in ths: th.join()
    dt = time.perf_counter() - t0
    for it in interps: it.close()
    return dt
1 op (main interp)     : 0.394s
k=2: serial=0.682s  2-interp-parallel=0.368s  speedup_vs_serial=1.85x
k=4: serial=1.441s  4-interp-parallel=0.348s  speedup_vs_serial=4.15x

4 个子解释器在 4 个线程上拿到 4.15x 加速(几乎线性),因为每个子解释器持有独立的 GIL,互不阻塞。这与 5.1 里普通多线程的 1.0x 形成鲜明对照——同样是标准构建,同样是 threading,换一种执行单元就能真并行。

不过子解释器有硬约束:对象不能随意跨解释器共享(iv.is_shareable() / NotShareableError 就是为这个设计的),通信要走 iv.create_queue()。它更像「轻量级进程」,适合任务彼此独立的 CPU 密集场景,而不是共享大量可变状态的多线程程序。

5.2.8 迁移判断框架

由于本机无法实测自由线程,这里给一个纯逻辑的评估清单,帮你在升级前做判断:

  1. 负载是否真的受益:只有 CPU 密集、且是纯 Python 或已适配的 C 扩展,才可能从自由线程获益;I/O 密集在标准构建上已经能加速(见 5.1.3),不必迁移。
  2. 依赖里有多少 C 扩展:任一未声明 Py_mod_gil 的扩展都会让 GIL 自动回退,迁移收益归零。
  3. 能否接受单线程 5%~10% 的回退:对延迟敏感的单线程服务要谨慎。
  4. 内存预算:永生字符串 + 更大的对象头会让常驻内存上升。

下一节 多进程、共享内存与 IPC 选型 会给出另一条不依赖解释器演进的多核路线。

小结

  1. 本机(3.14.6)是标准构建:Py_GIL_DISABLED=0、_is_gil_enabled()=True、PYTHON_GIL=0 与 -X gil=0 均报 fatal error。本节性能数字均来自官方文档,非本机实测。
  2. PEP 703 给每个对象加了 ob_ref_local / ob_ref_shared / PyMutex 等字段,用偏向引用计数让属主线程走无原子操作的快路径。
  3. 延迟引用计数(按对象标志位)、永生对象、dict/list 的乐观访问三层叠加,把去锁代价尽量从单线程移走。
  4. 自由线程不消除竞态:frame f_locals、共享迭代器都被官方列为不安全;应用层仍需自己加锁。
  5. 导入未声明 Py_mod_gil 的 C 扩展会自动把 GIL 开回来;单线程开销官方口径约 5%~10%(pyperformance 在 macOS aarch64 约 1%)。
  6. 不用等自由线程:3.14 的 PEP 734 子解释器(concurrent.interpreters)在标准构建上本机实测 4 线程 4.15x,每个子解释器持有独立 GIL,代价是对象不能随意跨解释器共享。

本节讲的是「未来的方向」,下一节 多进程、共享内存与 IPC 选型 讲的是「今天的多核手段」。

阅读导航:上一节:GIL 的实现与影响边界 · 下一节:多进程、共享内存与 IPC 选型 。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「python」更多文章

  1. 《Python高级编程》目录
  2. 《Python高级编程》11.3 PEP 流程与版本迁移策略
  3. 《Python高级编程》11.2 嵌入式与自由线程运行时