Python 对自由线程的支持¶
从 3.13 发布版开始,CPython 支持 free threading 的 Python 构建,其禁用 global interpreter lock (GIL)。自由线程化的执行允许在可用的 CPU 核心上并行运行线程,充分利用可用的处理能力。 尽管并非所有软件都能自动地从中受益,但是考虑到线程设计的程序在多核硬件上运行速度会更快。
某些第三方包,特别是带有 extension module 的包,可能尚不适用于自由线程构建版,并将重新启用 GIL。
本文档描述了自由线程对 Python 代码的影响。请参阅 自由线程的 C API 扩展支持 了解如何编写支持自由线程构建的 C 扩展。
参见
PEP 703 —— 查阅《在 CPython 中使全局解释器锁成为可选项》以了解对自由线程 Python 的整体描述。
安装¶
从 Python 3.13 开始,官方 macOS 和 Windows 安装器提供了对可选安装自由线程 Python 二进制文件的支持。安装器可在 https://www.python.org/downloads/ 获取。
有关其他平台的信息,请参阅 Installing a Free-Threaded Python,这是一份由社区维护的针对安装自由线程版 Python 的安装指南。
当从源码构建 CPython 时,应使用 --disable-gil 配置选项以构建自由线程 Python 解释器。
识别自由线程 Python¶
要判断当前解释器是否支持自由线程,可检查 python -VV 和 sys.version 是否包含 "free-threading build"。新的 sys._is_gil_enabled() 函数可用于检查在运行进程中 GIL 是否确实被关闭。
sysconfig.get_config_var("Py_GIL_DISABLED") 配置变量可用于确定构建是否支持自由线程。 如果该变量设置为 1,则构建支持自由线程。这是与构建配置相关的决策的推荐机制。
自由线程版 Python 中的全局解释器锁¶
CPython 的自由线程构建版支持在运行时使用环境变量 PYTHON_GIL 或命令行选项 -X gil 选择性地启用 GIL。
GIL 也可能在导入未显式标记为支持自由线程模式的 C-API 扩展模块时被自动启用。在这种情况下将会打印一条警告。
在单独软件包的文档以外,还有下列网站在追踪热门软件包对自由线程模式的支持状态:
线程安全¶
CPython 的自由线程构建旨在在 Python 层面提供与默认启用 GIL 的构建类似的线程安全行为。内置类型(如 dict、list 和 set)使用内部锁来防止并发修改,其行为方式与 GIL 类似。内置类型所提供的保证请参阅 线程安全性保证。
备注
建议尽可能使用 threading.Lock 或其他同步原语,而不是依赖内置类型的内部锁。
已知的限制¶
本节介绍自由线程 CPython 构建的已知限制。
永生化¶
在自由线程构建中,某些对象属于 immortal 对象。永生对象不会被释放且具有永远不会被修改的引用计数。 这样做是为了避免发生可能妨碍高效的多线程伸缩调整的引用计数争夺问题。
对于 3.14 发布版,永生对象只限于:
代码常量:数字字面值,字符串字面值,以及由其他常量组成的元组字面值。
由
sys.intern()所内化的字符串。
帧对象¶
从一个 帧对象 访问 frame.f_locals 是不安全的,如果该帧目前是在另一个线程中执行的话,这样做可能会导致解释器崩溃。
迭代器¶
从多个线程并发地访问同一个迭代器对象通常不是线程安全的,可能导致各个线程中出现重复或丢失的元素。
单线程性能¶
自由线程构建在执行 Python 代码时相比默认启用 GIL 的构建会有额外的开销。具体的开销取决于工作量和硬件环境。在 pyperformance 基准测试套件中,平均开销增幅在 macOS aarch64 上约为 1% 而在 x86-64 Linux 系统上约为 8%。
行为的变化¶
本节描述 CPython 在自由线程构建时的行为变化。
上下文变量¶
在自由线程构建中,thread_inherit_context 标志默认设置为 true,这会导致使用 threading.Thread 创建的线程以 start() 的调用程序的 Context() 的副本启动。在默认启用 GIL 的构建中,该标志默认为 false,因此线程以空 Context() 启动。
警告过滤器¶
在自由线程构建中,context_aware_warnings 标志默认设置为 true。在默认启用 GIL 的构建中,该标志默认设置为 false。如果该标志为 true,则 warnings.catch_warnings 上下文管理器使用上下文变量用于警告过滤器。如果该标志为 false,则 catch_warnings 修改全局过滤器列表,这不是线程安全的。详情请参阅 warnings 模块。
增加的内存使用¶
自由线程构建版相比默认构建版通常会使用更多内存。 这有很多原因,大部分是由于设计上的决定。
所有内生字符串都是永生对象¶
对于现代的 Python 版本(从 2.3 版开始),内生字符串 (例如通过 sys.intern()) 不会使它成为永生对象。 作为替代,如果对某字符串的最后一次引用消失,它将从内生字符串表中移除。 对于自由线程构建版来说则并非如此而是任何内生字符串都将成为永生对象,会保持存活直到解释器关闭。
非 GC 对象具有更大的对象标头¶
自由线程构建版本采用不同的 PyObject 结构。与默认构建版本中将与 GC 相关的信息在 PyObject 结构之前分配不同,自由线程构建版本将 GC 相关信息作为普通对象头的一部分。例如,在 AMD64 平台上,自由线程构建版本的 None 占用 32 字节,而默认构建版本则占用 16 字节。对于 GC 对象(如字典和列表),两种构建版本的尺寸相同,因为自由线程构建版本不会为 GC 信息额外占用空间。
QSBR 可能会延迟释放内存¶
为了安全地实现无锁数据结构,采用了一种安全的内存回收方案,称为基于静默状态的回收(QSBR)。这意味着,支持无锁访问的数据结构所使用的内存将采用 QSBR,该方案会延迟释放操作,而不是立即释放内存。这两种数据结构的一个例子是列表对象和字典键对象。有关 QSBR 实现方式的更多细节,请参阅CPython源码树中的 InternalDocs/qsbr.md。运行 gc.collect() 应会使所有由 QSBR 持有的内存实际释放。请注意,即使 QSBR 释放了内存,底层的内存分配器也可能不会立即将内存返回给操作系统,因此进程的驻留集大小(RSS)可能不会减少。
mimalloc 内存管理器 vs. pymalloc¶
默认构建版本通常会为小规模分配(512 字节或更小)使用“pymalloc”内存分配器。自由线程构建版本则不使用 pymalloc,而是使用“mimalloc”分配器为所有 Python 对象进行分配。pymalloc 分配器具有以下特性,有助于保持较低的内存占用:每个分配块的低开销、有效的内存碎片化抑制,以及将空闲内存快速返回给操作系统。mimalloc 分配器在这些方面同样表现良好,但可能会产生略高的开销。
在自由线程构建中,mimalloc 在多个独立的堆中管理内存(目前为四个)。例如,所有支持 GC 的对象都从各自的堆中分配。使用独立的堆意味着一个堆中的空闲内存不能用于分配另一个堆所需的内存。此外,某些堆被配置为在释放支撑该堆的内存(在 mimalloc 术语中称为“页面”)时使用 QSBR(基于静默状态的回收机制)。使用 QSBR 会在某个页面被释放与内存页面真正释放之间产生延迟,无论是用于新的分配还是返回给操作系统。
mimalloc 分配器还会延迟将已释放的内存返回给操作系统。您可以通过将环境变量 MIMALLOC_PURGE_DELAY 设置为 0 来缩短这一延迟。请注意,这很可能会降低分配器的性能。
自由线程引用计数可能延长对象的存活时间¶
在默认构建版本中,当对象的引用计数达到零时,通常会立即释放该对象。自由线程构建版本采用“偏向式引用计数”机制:对于由当前线程“拥有”的对象,使用快速路径;对于其他对象,则使用慢速路径。更多细节请参阅 PEP 703。每当对象的引用计数进入“ queued”状态时,释放操作可以延迟执行。 queued 状态会从字节码评估器的“eval breaker”部分中清除。
这种自由线程构建还支持一种不同的引用计数模式,称为“延迟引用计数”。该模式通过以每个对象为单位设置一个标志来启用。以下类型支持延迟引用计数:
模块对象
模块顶层函数
在类作用域中定义的类方法
描述符对象
由
threading.local创建的线程局部对象
当启用延迟引用计数时,来自 Python 函数栈的引用不会被计入引用计数。这种机制降低了引用计数的开销,尤其对于由多个线程使用的对象而言。由于栈引用不会被计入,采用延迟引用计数的对象在其内部引用计数归零时并不会立即被释放;相反,它们会在下一次 GC 运行期间被检查,若未发现指向它们的栈引用,则会被释放。这意味着这些对象由 GC 释放,而非像通常情况那样在引用计数归零时释放。
按线程进行的引用计数可能会延迟对象的释放¶
为避免频繁共享对象的引用计数字段发生竞争,自由线程构建版本还针对部分选定的对象类型采用“每线程引用计数”机制。每个线程并不更新单一的共享引用计数,而是维护一个本地的引用计数数组,该数组以分配给对象的唯一 ID 作为索引。只有当对象的本地计数降至零时,才会通过累加各线程的计数来计算真实的引用计数。目前,每线程引用计数机制适用于以下场景:
堆类型对象(在 Python 中创建的类)
代码对象
模块对象的
__dict__
由于对象的每线程计数必须在释放前合并回对象,使用每线程引用计数的对象通常会比在默认构建中更晚被释放。 具体而言,这样的对象往往要等到引用它的线程处于安全点位(例如,在字节码求值器的 "eval breaker" 节中)或退出时才会被释放。 运行 gc.collect() 将合并每线程计数并允许这些对象被释放。