编程 别只盯着「删掉那把锁」:PEP 703 改了 PyObject 的哪些字段,PEP 779 用什么硬指标判定转正

2026-10-05 00:03:48

别只盯着「删掉那把锁」:PEP 703 改了 PyObject 的哪些字段,PEP 779 用什么硬指标判定转正

站内 #7182 讲的是生产迁移的排查与验收;这篇只拆设计层:PEP 703 动的是 CPython 的哪些内部结构,以及 PEP 779 用哪些可量化指标决定 free-threaded 构建能否进入 Phase II。

官方文档:

  • PEP 703 – Making the Global Interpreter Lock Optional in CPython:https://peps.python.org/pep-0703/
  • PEP 779 – Criteria for supported status for free-threaded Python:https://peps.python.org/pep-0779/
  • free threading howto:https://docs.python.org/3/howto/free-threading-python.html
  • 第三方包适配追踪:https://py-free-threading.github.io/tracking/

PEP 703 的状态是 Final,Standards Track,作者 Sam Gross,Python-Version 3.13,2023-10-24 resolution。它已经是历史文档,最新权威内容以 howto 为准。Steering Council 接受它时附了条件:推进要渐进、尽量少破坏,并且允许回滚全部改动。

构建开关与 ABI

GIL 仍是 CPython 构建和 python.org 下载的默认。新增的 configure 选项是 --disable-gil。以此构建时,CPython 在 Python/patchlevel.h 定义 Py_GIL_DISABLED 宏,ABI tag 里带上字母 t(threading)。该构建仍支持运行时选择启用 GIL,入口是 PYTHONGIL 环境变量和 Py_mod_gil 槽。

去 GIL 要重写大量内部实现,但公开的 Python API 与 C API 改动相对少。实现改动分四类:引用计数、内存管理、容器线程安全、锁与原子 API。

引用计数:BRC + immortalization + 有限的 deferred

引用计数靠三种技术组合:biased reference counting(BRC,偏置引用计数)、immortalization(不朽化)、有限形式的 deferred reference counting(延迟引用计数)。

BRC 的前提观察是:即使是多线程程序,多数对象也只被单个线程访问。每个对象关联一个 owning thread(创建它的线程),来自 owning thread 的引用计数操作用非原子指令改「local」计数,其他线程用原子指令改「shared」计数,从而避免大量昂贵的原子读改写。BRC 论文把这三样打包进单个 64 位字段;本 PEP 改用对象头里三个独立字段,既避开引用计数溢出问题,也支持更快的释放路径。

提议的 PyObject 结构体(struct _object):

_PyObject_HEAD_EXTRA
uintptr_t ob_tid;            // owning thread id (4-8 bytes)
uint16_t __padding;          // reserved for future use (2 bytes)
PyMutex ob_mutex;            // per-object mutex (1 byte)
uint8_t ob_gc_bits;          // GC fields (1 byte)
uint32_t ob_ref_local;       // local reference count (4 bytes)
Py_ssize_t ob_ref_shared;    // shared reference count and state bits (4-8 bytes)
PyTypeObject *ob_type;

ob_tid / ob_ref_local / ob_ref_shared 供 BRC 使用;ob_gc_bits 存原先放在 PyGC_Head 里的 GC 标志;ob_mutex 提供单字节的每对象锁。

Immortalization 针对 interned 字符串、小整数、静态分配的 PyTypeObject、True/False/None 这类活满整个进程生命周期的对象:把它们的 ob_ref_local 设为 UINT32_MAX 即标记为 immortal,Py_INCREF / Py_DECREF 变成 no-op,避免多线程并发访问这些对象引用计数字段时的争用。该方案与 3.12 采纳的 PEP 683 很像,但为了配合 BRC 与 deferred ref counting,引用计数字段里的位表示略有不同。

BRC 的快慢路径由 ob_tid 决定:对当前线程 owned 的对象走 fast-path,其他走 slow-path;ob_tid 为 0 表示对象不属于任何线程。ob_ref_local 存 local 计数与两个标志(最高两位表示 immortal 或使用 deferred ref counting)。ob_ref_shared 存 shared 计数,最低两位存引用计数状态,因此 shared 计数左移两位——用最低位是因为 shared 计数可能临时为负,线程间 incref/decref 可能不平衡。

状态位:

0b00 - default
0b01 - weakrefs
0b10 - queued
0b11 - merged

状态是递进的,对象生命周期内可迁移到任何数值更高的状态。只有 default 和 merged 状态可被释放,其他状态必须先转到 merged 再释放。

内存管理

pymalloc 不是线程安全的,去 GIL 需替换为 mimalloc——通用、线程安全、小分配性能好的分配器。此前用于加速元组、数字等小对象分配的 CPython Free Lists 同样不是线程安全的,--disable-gil 构建下必须禁用。另外,只有 Python 对象使用 object 域分配,且所有 Python 对象都必须用该域分配:这条以前是 best practice,free-threaded 下是硬要求。

循环垃圾回收

无 GIL 时需要一种机制保证循环检测期间引用计数稳定:跑 Python 代码的线程必须被暂停,确认引用与引用计数稳定、识别出环之后再恢复其他线程,也就是 Stop-the-World。此外还涉及 Generations 以及与 deferred/biased ref counting 的集成。

按 howto 的描述,free-threaded 构建用 biased reference counting,对当前线程 owned 的对象走 fast-path、其他走 slow-path;对象引用计数进入 queued 态时释放可被推迟,queued 态在字节码求值器的 eval breaker 段清除。

容器线程安全

容器会内部加锁,例如 PyList_Append。

Borrowed References(借用引用)在无 GIL 下有时不安全,因为另一线程可能并发修改容器。新 API PyDict_FetchItem 返回 new references,老 API PyDict_GetItem 返回 borrowed references。

Python Critical Sections 提供 Py_BEGIN_CRITICAL_SECTION。对 list 和 dict 的访问还会乐观地避免加锁(依赖 QSBR),这需要 mimalloc 页复用相关的改动配合。

Py_mod_gil 槽与运行时覆盖

--disable-gil 构建下,加载扩展时 CPython 检查新的 PEP 489 风格 Py_mod_gil 槽:

  • 设为 Py_mod_gil_not_used,扩展正常加载。
  • 未设置时,解释器暂停所有线程并启用 GIL 后再继续,同时打印可见警告,指明扩展名、为何启用 GIL、以及覆盖它的步骤。

PYTHONGIL 环境变量可在运行时覆盖:PYTHONGIL=0 强制禁用 GIL(覆盖模块槽逻辑),PYTHONGIL=1 强制启用 GIL。howto 另补充命令行 -X gil;导入未显式标记支持 free threading 的 C-API 扩展模块时,GIL 也可能被自动启用并打印警告。

Rejected Ideas / Open Issues 摘录

为什么不废弃 PyDict_GetItem。 PEP 不打算把 borrowed 版本变成编译错误或废弃。很多 PyDict_GetItem / List_GetItem 用法无 GIL 也安全(比如读取 kwargs);作者早期尝试整体替换 borrowed→new 引用,容易引入新的引用计数 bug,风险大于收益。扩展作者可按需替换。

为什么不复用 PEP 683 的 immortalization。 PEP 683 尝试兼容 stable ABI(abi3),把 immortal 位放在第二高位;本方案用 BRC + deferred ref counting,引用计数字段布局不同(两个字段而非一个),immortal 位改放低位,所以不直接复用。

Open Issues。 Python Build Modes:作者希望两种构建模式是临时的,最终合并为运行时控制、可能默认禁用;另有 Improved Specialization、Integration、Mitigations for Single-Threaded Performance。

Backwards Compatibility。 --disable-gil 构建模式与标准构建 ABI 不兼容。

Gilectomy 是 Larry Hastings 的去 GIL 项目,同样支持同解释器内多线程并行(free-threading),用细粒度锁;本 PEP 的参考实现在单线程性能与扩展性上优于 Gilectomy。其余相关工作包括 Per-Interpreter GIL(PEP 684)、PyParallel、python-safethread、Greg Stein 的 free-threading patch、Jython/IronPython、PyPy-STM。

PEP 779:三阶段路线图

PEP 779 状态 Final,Standards Track,作者 Thomas Wouters、Matt Page、Sam Gross,Python-Version 3.14,2025-06-16 resolution。SC 接受了它,并附了一份非穷尽的 phase II 期间待处理要求清单。

PEP 703 被接受时,SC 把去 GIL 工作划成三个阶段:

  • Phase I:实验阶段,free-threaded 构建通过 build-time 选项启用,不应是任何地方的默认安装;至少一个大版本包含该实验构建,让第三方包测试;明确是 experimental、不支持生产、可能被回滚。3.13 早期开始。
  • Phase II:free-threaded 构建被官方支持,但仍可选。
  • Phase III:free-threaded 构建成为默认(初期仍可禁用);再往后当确认不再被广泛使用,开始讨论彻底移除 GIL 构建。

当时进入下一阶段的标准故意留模糊,PEP 779 为进入 Phase II 设定了明确预期与要求。

为什么这些指标算数

PEP 703 要可接受,应当 desirable、stable、maintainable、performant(CPU 与内存),最好还有 Stable ABI(同一 wheel 可用于 free-threaded 与 with-GIL 构建)。

  • Desirability:free-threaded 潜力巨大,但不是简单 drop-in;有些代码需重新设计以充分利用并避开性能陷阱。
  • Stability:大部分新 API 设计在 3.13 已定,第三方包已在用(见 https://py-free-threading.github.io/tracking/)。3.14 继续加了便利函数、替换了原先依赖 GIL 保证线程安全的 API,但未破坏 3.13 的 free-threaded API。
  • Maintainability:PEP 703 大部分设计相对简单,复杂度藏在既有 C API 后;无锁 list/dict API(依赖 QSBR)与避免死锁的 critical section 实现复杂,但对外是易用 API。
  • Performance:以 pyperformance 测,free-threaded 相对 with-GIL 当前线性性能损失约 10%(macOS 约 3%);还有若干 PR 在途,目标在 Linux/Windows 上舒适地降到 10% 以下。
  • Memory use:free-threaded 目前内存使用高约 15–20%(geometric mean,pyperformance)。尚未花大力气降低;高效安全的 free-threading 客观上要付出更高内存代价。
  • Stable ABI:SC 提到可能是 phase II 的潜在要求,但有先有鸡还是先有蛋的问题。

进入 Phase II 的具体标准

  • Acceptable performance:SC 曾预期 free-threaded 约慢 10–15%,非硬指标。当前约 10%,预期不会更慢;但 phase II(非默认翻转)设定 15% 为硬性能目标。
  • Acceptable memory use:之前没怎么讨论;提议 phase II 目标为 20%(geometric mean,pyperformance)。phase III 需要社区输入决定内存与 CPU 性能的权衡落点。
  • Proven, stable APIs:现有 API 已被显著采纳,未被迫大改;未来改动遵循 PEP 387 变更策略。
  • Internal documentation:需补齐 free-threaded 内部机制的入门文档,3.14 达到应不成问题。

满足上述标准后,认为 3.14 是进入 Phase II 的合适时间。注意这些只是进入 phase II 的要求;让 free-threaded 成为默认(phase III)是另一回事,取决于社区支持、意愿与明确收益,留给未来 PEP。

Open Issues:Stable ABI 是否应为 free-threaded「supported」状态的强要求?

推荐文章

程序员茄子在线接单