CPython 无 GIL 构建(Free-Threading)实战指南:安装、识别、线程安全与内存行为详解
CPython 自 3.13 起提供了 free threading(无 GIL)构建,允许线程真正并行地运行在多个 CPU 核心上,充分利用多核硬件的算力。本文基于 CPython 官方 How-to 文档 Doc/howto/free-threading-python.rst 并结合当前仓库源码展开:你将掌握如何安装/编译 free-threaded Python、如何识别当前解释器是否为无 GIL 构建、如何运行时控制 GIL 开关、内置类型的线程安全语义,以及无 GIL 构建在永生对象(immortalization)、QSBR 内存回收、mimalloc 分配器、偏差引用计数等方面与默认构建的行为差异,从而为编写和移植多线程 Python 程序提供可靠依据。
一、什么是 Free-Threading 构建
从 3.13 版本开始,CPython 支持一种称为 :term:free threading 的构建形态:全局解释器锁(GIL)被默认禁用。线程可以真正并行执行,Python 程序能够吃满多核 CPU。需要明确两点前提:
- 并非所有软件都能自动受益:只有从设计上就使用多线程的程序,才能在多核硬件上跑得更快;
- 第三方扩展模块可能未就绪:部分带 C 扩展模块(extension module)的第三方包尚不支持 free-threaded 构建,导入时可能会重新启用 GIL。
若你的目标是编写支持无 GIL 构建的 C 扩展,可继续阅读仓库中的姊妹文档 Doc/howto/free-threading-extensions.rst;PEP 703(Making the Global Interpreter Lock Optional in CPython)则是整个 free-threaded Python 的总体设计说明。
二、安装 Free-Threaded Python
2.1 官方安装器(macOS / Windows)
从 Python 3.13 起,官方 macOS 和 Windows 安装器可选地提供 free-threaded Python 二进制,可从 python.org 官方下载页获取。
2.2 从源码编译
从源码构建 CPython 时,应使用 --disable-gil 配置选项。当前仓库 configure.ac 中该选项的处理逻辑如下(约 L1788–L1804):
AC_ARG_ENABLE([gil],
[AS_HELP_STRING([--disable-gil], [enable support for running without the GIL (default is no)])],
[AS_VAR_IF([enable_gil], [yes], [disable_gil=no], [disable_gil=yes])], [disable_gil=no])
if test "$disable_gil" = "yes"
then
AC_DEFINE([Py_GIL_DISABLED], [1], [Define if you want to disable the GIL])
# Add "t" for "threaded"
ABIFLAGS="${ABIFLAGS}t"
ABI_THREAD="t"
fi
从源码结构看,--disable-gil 会带来三个可观测的构建特征:
- 定义宏
Py_GIL_DISABLED=1,作为整个代码库区分两种构建形态的编译期开关; - 在 ABI 标签中追加
t(threaded),因此无 GIL 构建的python3.13t与python3.13拥有不同的site-packages,二者不共用同一解释器目录; - configure.ac 还约束了若干组合限制:
--disable-gil不能与--with-trace-refs一起使用,且要求必须启用 mimalloc 分配器(--disable-gil requires mimalloc memory allocator),这与下文内存部分相呼应。
其他平台(Linux、BSD 等)的安装方式可参考社区维护的 Installing a Free-Threaded Python 指南(py-free-threading.github.io)。
三、如何识别 Free-Threaded Python
官方文档给出了三个层次的判断方法,覆盖"进程级"与"构建级"两种需求:
| 手段 | 含义 | 适用场景 |
|---|---|---|
python -VV / sys.version 包含 "free-threading build" |
当前解释器是否 free-threading 构建 | 快速人工确认 |
sys._is_gil_enabled() |
当前进程里 GIL 是否实际被禁用 | 运行时检查(注意 GIL 可能被关闭后又因导入扩展而自动开启) |
sysconfig.get_config_var("Py_GIL_DISABLED") == 1 |
该构建是否支持 free threading | 推荐的构建级判断机制(如条件导入、条件安装依赖包) |
源码层面可对应到:
- Python/getversion.c 中
buildinfo_format = "%.80s free-threading build (%.80s) %.80s",即版本字符串里 "free-threading build" 一词的出处; - Python/sysmodule.c 中
sys._is_gil_enabled_impl的实现:无 GIL 构建下返回_PyEval_IsGILEnabled(tstate)的运行时状态,普通构建恒返回 1; Py_GIL_DISABLED正是第二节中configure注入的宏,经sysconfig配置变量暴露给 Python 层。
四、无 GIL 构建下的 GIL:运行时可控
free-threaded 构建支持在运行时选择重新启用 GIL,手段是环境变量 PYTHON_GIL 或命令行选项 -X gil,取值 0/1。Python/initconfig.c 的 config_read_gil 展示了校验与分派逻辑:
static PyStatus
config_read_gil(PyConfig *config, size_t len, wchar_t first_char)
{
if (len == 1 && first_char == L'0') {
#ifdef Py_GIL_DISABLED
config->enable_gil = _PyConfig_GIL_DISABLE;
#else
return _PyStatus_ERR("Disabling the GIL is not supported by this build");
#endif
}
else if (len == 1 && first_char == L'1') {
...
}
else {
return _PyStatus_ERR("PYTHON_GIL / -X gil must be \"0\" or \"1\"");
}
return _PyStatus_OK();
}
从源码结构看,-X gil 在非无 GIL 构建下请求"禁用"会直接报错,说明该开关只在 free-threading 构建中真正有意义。
4.1 导入未声明兼容的 C 扩展时 GIL 会自动开启
如果某个 C-API 扩展模块没有显式声明支持 free threading,CPython 会自动启用 GIL 并打印一条 RuntimeWarning。Python/import.c 中的 _PyImport_EnableGILAndWarn 给出了确切措辞:
"The global interpreter lock (GIL) has been enabled to load module '%U', which has not declared that it can run safely without the GIL. To override this behavior and keep the GIL disabled (at your own risk), run with
PYTHON_GIL=0or-Xgil=0."
也就是说,你可以用 PYTHON_GIL=0 / -X gil=0 强制保持 GIL 关闭,但文档明确提示这是"at your own risk"(自担风险)。关于流行包对 free threading 的支持状态,可参考官方文档提到的两个社区跟踪站点:py-free-threading.github.io/tracking 与 hugovk.github.io/free-threaded-wheels(以及各包自身的文档)。
五、线程安全:内置类型的语义
free-threaded 构建的目标是在 Python 层提供与默认 GIL 构建相近的线程安全行为:dict、list、set 等内置类型使用内部锁来防止并发修改,行为上模拟 GIL 提供的保护。但必须注意措辞边界——Python 历史上从未对"并发修改这些内置类型会发生什么"做出行为保证,因此官方文档明确:
这应当被视为当前实现现状的描述,而非对当前或未来行为的保证。
实践建议(与官方 note 一致):尽可能使用 threading.Lock 或其他同步原语,而不是依赖内置类型的内部锁。
六、已知限制
官方文档列出以下 free-threaded 构建的已知限制,理解它们对编写并行代码至关重要。
6.1 永生对象(Immortalization)
无 GIL 构建中,部分对象是**永生(immortal)**的:不会被释放,引用计数也永不被修改。其目的是避免引用计数争用(contention)——若所有线程都竞争同一对象的引用计数缓存行,多线程扩展性会被严重拖垮。截至 3.14 发布,永生化仅限于:
- 代码常量:数字字面量、字符串字面量,以及由其他常量组成的元组字面量;
- 通过
sys.intern完成驻留(interning)的字符串。
6.2 帧对象(Frame objects)
当某个帧正在另一个线程中执行时,从该帧对象访问 frame.f_locals 是不安全的,可能导致解释器崩溃。做跨线程调试、抓栈快照时必须留意这一点。
6.3 迭代器(Iterators)
从多个线程并发访问同一个迭代器对象通常不是线程安全的,各线程可能看到重复或缺失的元素。多线程场景下应让每个线程持有独立的迭代器(例如先 list() 化再分片,或使用独立迭代)。
6.4 单线程性能开销
free-threaded 构建执行 Python 代码的开销高于默认构建,幅度取决于工作负载和硬件。官方文档给出的基准数据:在 pyperformance 基准套件上,平均开销约为 macOS aarch64 上 1% 到 x86-64 Linux 系统上 8%。因此,如果你的程序是单线程 CPU 密集型,默认 GIL 构建仍是更优选择。
七、行为变化(Behavioral Changes)
7.1 Context variables:线程继承上下文
- 无 GIL 构建:
sys.flags.thread_inherit_context默认为真,threading.Thread.start()创建的新线程会携带调用者contextvars.Context的一份副本; - 默认 GIL 构建:该标志默认为假,新线程从空 Context 开始。
如果你在默认构建中已经依赖"子线程继承 Context",需要显式设置该标志以保持行为一致。
7.2 Warning filters:上下文感知的警告过滤器
- 无 GIL 构建:
sys.flags.context_aware_warnings默认为真,warnings.catch_warnings使用 context variable 存放警告过滤器,因此对每个线程/上下文天然隔离; - 默认 GIL 构建:该标志默认为假,
warnings.catch_warnings修改的是全局过滤器列表,这在多线程下并非线程安全。
这是无 GIL 构建下更安全的默认行为,详见 warnings 模块文档。
7.3 内存占用:无 GIL 构建通常更耗内存
官方文档从五个设计层面解释了内存增长的原因,下面逐项展开。
(1)所有驻留字符串都是永生的
现代 Python(2.3 起)中,sys.intern 并不会让字符串永生;最后一个引用消失时,字符串会从驻留表中移除。但 free-threaded 构建不同:任何驻留字符串都会成为永生对象,存活到解释器退出。
(2)非 GC 对象的对象头更大
free-threaded 构建使用不同的 PyObject 结构:默认构建把 GC 相关信息放在 PyObject 结构之前分配,而无 GIL 构建把 GC 信息并入常规对象头内。官方文档给出的量化例子:AMD64 上 None 在无 GIL 构建占 32 字节,而默认构建占 16 字节;而 GC 对象(dict、list 等)两种构建大小相同,因为无 GIL 构建没有为 GC 信息额外占用空间。
(3)QSBR 会延迟释放内存
为安全地实现无锁(lock-free)数据结构,CPython 使用安全内存回收(SMR)方案——QSBR(quiescent state-based reclamation)。依靠 QSBR 的内存(如 list 对象、dict 的 keys 对象这类支持无锁访问的数据结构)在释放时是延迟释放而非立即释放。CPython 源码树中的 InternalDocs/qsbr.md 详细说明了 QSBR 的实现。要点:
- 运行
gc.collect()应促使 QSBR 持有的内存真正被释放; - 但即使 QSBR 释放了内存,底层分配器未必立即把内存归还操作系统,因此进程 RSS 可能不下降。
(4)mimalloc 替代 pymalloc
- 默认构建:小对象(≤ 512 字节)通常走 pymalloc,其特点是每块开销小、能有效抑制碎片、快速归还空闲内存;
- 无 GIL 构建:不使用 pymalloc,所有 Python 对象都经 mimalloc 分配。mimalloc 表现也不错但开销略高(且如第二节源码所示,
--disable-gil强制要求 mimalloc)。
从源码结构看,mimalloc 在无 GIL 构建中把内存划分为多个独立堆(目前 4 个),例如所有支持 GC 的对象从专属堆分配。由此产生两个后果:一个堆的空闲内存不能服务另一个堆的分配请求;且部分堆释放页(mimalloc 术语中的 "page")时使用 QSBR,从"该页所有块都被释放"到"页被归还"之间存在延迟。此外,mimalloc 本身也会延迟归还空闲内存给 OS,可通过把环境变量 MIMALLOC_PURGE_DELAY 设为 0 缩短延迟——代价是分配器性能下降。
(5)无 GIL 的引用计数会让对象活得更久
-
偏差引用计数(biased reference counting):无 GIL 构建对"当前线程持有"的对象走快速路径,对共享对象走慢路径(细节见 PEP 703)。引用计数一旦进入"queued"状态,释放可能被推迟;queued 状态在字节码求值器的 eval breaker 段被清除。
-
延迟引用计数(deferred reference counting):按对象逐位开启的另一种模式,Python 函数栈上的引用不计入引用计数,从而显著降低多线程序列的引用计数开销。适用于以下类型:
- 模块对象(module objects);
- 模块顶层函数;
- 类作用域中定义的类方法;
- 描述符对象(descriptor objects);
threading.local创建的线程局部对象。
代价是:这些对象在内部引用计数归零时不会立即释放,而是等待下一次 GC 运行检查栈引用、确认无引用后由 GC 释放——这与"引用计数归零即释放"的常规语义不同。
-
每线程引用计数(per-thread reference counting):为避免高频共享对象上单一共享引用计数字段的争用,无 GIL 构建对少数类型改用每线程计数数组(按对象唯一 id 索引),只有当对象局部计数降为 0 时才汇总各线程计数得到真实计数。当前用于:
- 堆类型对象(Python 中创建的类);
- 代码对象(code objects);
- 模块对象的
__dict__。
由于每线程计数必须先合并回对象才能释放,这类对象通常比默认构建释放得更晚——一般要等到持有它的线程到达安全点(如 eval breaker)或退出线程。运行
gc.collect()会合并每线程计数,使这些对象得以释放。
八、实操要点速查
| 需求 | 做法 |
|---|---|
| 编译无 GIL Python | ./configure --disable-gil(自动要求 mimalloc,ABI 加 t 后缀) |
| 确认构建类型 | python -VV 查看 "free-threading build";sysconfig.get_config_var("Py_GIL_DISABLED") |
| 运行时强制关 GIL | PYTHON_GIL=0 或 python -X gil=0(自担风险) |
| 运行时开 GIL | PYTHON_GIL=1 或 python -X gil=1 |
| 确认进程当前 GIL 状态 | sys._is_gil_enabled() |
| 降低 RSS 延迟 | MIMALLOC_PURGE_DELAY=0(会牺牲分配器性能) |
| 促使 QSBR / 延迟引用对象释放 | gc.collect() |
| 编写兼容扩展 | 阅读 Doc/howto/free-threading-extensions.rst |
适用前提与限制小结:以上所有行为均以**当前仓库(3.14 时代的 CPython 主分支源码)**为准;"free-threading build" 标识、-X gil、Py_GIL_DISABLED 均为 3.13+ 的能力。若你在 3.13 上使用,永生对象范围与 per-thread 计数等细节请以当时版本文档为准。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
