首页
/ CPython 无 GIL 构建(Free-Threading)实战指南:安装、识别、线程安全与内存行为详解

CPython 无 GIL 构建(Free-Threading)实战指南:安装、识别、线程安全与内存行为详解

2026-09-06 17:05:04作者:范垣楠Rhoda

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 程序提供可靠依据。

CPython 对象布局图,展示 PyObject 对象头及 GC 相关信息的内存结构

一、什么是 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 会带来三个可观测的构建特征:

  1. 定义宏 Py_GIL_DISABLED=1,作为整个代码库区分两种构建形态的编译期开关;
  2. 在 ABI 标签中追加 t(threaded),因此无 GIL 构建的 python3.13tpython3.13 拥有不同的 site-packages,二者不共用同一解释器目录;
  3. 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.cbuildinfo_format = "%.80s free-threading build (%.80s) %.80s",即版本字符串里 "free-threading build" 一词的出处;
  • Python/sysmodule.csys._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/1Python/initconfig.cconfig_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 并打印一条 RuntimeWarningPython/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=0 or -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 构建相近的线程安全行为dictlistset 等内置类型使用内部锁来防止并发修改,行为上模拟 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=0python -X gil=0(自担风险)
运行时开 GIL PYTHON_GIL=1python -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 gilPy_GIL_DISABLED 均为 3.13+ 的能力。若你在 3.13 上使用,永生对象范围与 per-thread 计数等细节请以当时版本文档为准。

登录后查看全文
热门项目推荐
相关项目推荐