Python free-threading 下扩展模块兼容清单
我第一次把一个依赖原生扩展的 Python 服务搬到 free-threaded 构建时,最容易误判的不是“代码能不能启动”,而是“启动后到底是不是无 GIL”。一个普通的 wheel 可能让安装成功,一个扩展也可能让导入成功,但这两件事都不能单独证明它已经获得并行执行能力。
这篇文章把兼容性拆成四层:解释器身份、wheel 产物、扩展模块声明、线程安全测试。按这个顺序检查,通常能很快知道问题是在包装阶段、导入阶段,还是已经进入并发语义阶段。
官方文档:https://docs.python.org/3/howto/free-threading-python.html
真正可以交付的 free-threading 兼容,不是“能 import”这一项,而是解释器确实处于 free-threaded 状态、依赖拿到了对应二进制、扩展明确声明支持,并且业务代码在双解释器矩阵下通过了并发测试。
先确认解释器和轮子是不是同一条线
CPython 从 3.13 开始提供 free-threaded 构建,3.14 文档已经把它作为可选的正式支持形态来描述。它和默认带 GIL 的 CPython 不是同一个 ABI 目标,因此不能只看 Python 的主版本号。
我现在会先区分两个问题:当前构建是否具备 free-threading 能力,以及这个进程此刻是否真的关闭了 GIL。前者适合用配置变量判断,后者适合用运行时函数确认:
import sys
import sysconfig
# Py_GIL_DISABLED=1 表示这个解释器构建支持 free-threading。
build_supports_free_threading = (
sysconfig.get_config_var("Py_GIL_DISABLED") == 1
)
# 运行时状态可能受命令行、环境变量或不兼容扩展影响。
gil_is_enabled = sys._is_gil_enabled()
print({
"version": sys.version,
"build_supports_free_threading": build_supports_free_threading,
"gil_is_enabled": gil_is_enabled,
})
这里要特别注意:构建支持 free-threading,不等于 GIL 已经关闭。官方文档说明,命令行选项或环境变量可以重新启用 GIL;导入没有声明 free-threaded 支持的 C API 扩展时,CPython 也可能在运行时重新启用 GIL。
第二个检查点是 wheel。free-threaded 构建的二进制通常会带 t 后缀,例如 Python 3.14 对应的解释器和 wheel 标签会出现 cp314t。因此我的依赖清单不会只写“支持 Python 3.14”,而会单独记录“是否发布了 free-threaded wheel、覆盖哪些平台、是否仍是 nightly”。

“能安装”与“能并发运行”是两张清单
我把依赖分成三类管理,这比单纯维护一个“支持/不支持”列表更实用。
| 依赖类型 | 首先看什么 | 不能据此推出什么 |
|---|---|---|
| 纯 Python 包 | 是否使用了隐含的 GIL 保护、共享可变状态和非线程安全第三方对象 | 纯 Python 不代表业务逻辑已经线程安全 |
| 带原生扩展的包 | 是否有对应的 cp3xx t wheel,扩展是否声明不使用 GIL | 有 free-threaded wheel 不代表所有 API 都支持并发调用 |
| 本地编译依赖 | 构建后端、编译宏、初始化方式、平台和 CI 是否覆盖 free-threaded 构建 | 本机编译通过不代表发布的 wheel 可复现 |
生态跟踪页适合用来发现哪些项目正在补齐 free-threading 支持,但它不是兼容性证书。对生产依赖,我还会回到项目自己的 release notes、wheel 文件名、测试配置和已知限制。
扩展模块必须显式声明自己不依赖 GIL
如果扩展没有声明 free-threaded 支持,导入时可能触发警告并让 GIL 重新启用。这个行为很安全,但会让“服务已经换成 free-threaded Python”的判断失真。
多阶段初始化的扩展可以在模块槽位中声明 Py_MOD_GIL_NOT_USED;单阶段初始化则需要在模块创建后调用 PyUnstable_Module_SetGIL。两条路径不能混写,兼容旧版本时还应使用版本或宏保护:
static PyModuleDef_Slot module_slots[] = {
/* 只有支持该 API 的 CPython 才声明 free-threaded 兼容。 */
#if PY_VERSION_HEX >= 0x030D0000
{Py_mod_gil, Py_MOD_GIL_NOT_USED},
#endif
{0, NULL}
};
PyMODINIT_FUNC PyInit_demo(void)
{
/* 多阶段初始化会读取上面的模块槽位。 */
return PyModuleDef_Init(&moduledef);
}
单阶段初始化的关键点不同:
PyMODINIT_FUNC PyInit_demo(void)
{
PyObject *module = PyModule_Create(&moduledef);
if (module == NULL) {
/* 模块创建失败时直接把异常交回 CPython。 */
return NULL;
}
#ifdef Py_GIL_DISABLED
/* 只在 free-threaded 构建声明“不使用 GIL”。 */
PyUnstable_Module_SetGIL(module, Py_MOD_GIL_NOT_USED);
#endif
return module;
}
这两个声明只是承诺“模块已经完成相应改造”,不会替你检查全局变量、借用引用、缓存对象或外部线程回调是否安全。声明应当放在真实的并发测试之后,而不是当作让测试变绿的开关。
封装层的开关不能漏在构建脚本里
很多项目并不直接手写 C API,而是通过 Cython、pybind11、PyO3 或 f2py 生成扩展。这时兼容性通常藏在构建参数里,代码审查不能只看 Python 层。
- Cython 3.1 起可以用
freethreading_compatible编译指令声明支持。 - pybind11 可以在
PYBIND11_MODULE中使用py::mod_gil_not_used()。 - PyO3 0.28 及更新版本的模块宏默认声明 free-threading 支持;如果代码还没有完成移植,应明确保留
gil_used = true。 - NumPy 的 f2py 可以通过
--freethreading-compatible声明 Fortran 扩展的兼容意图。
这些选项的共同点是:它们只负责把“模块愿意在无 GIL 环境工作”的信息传给构建和解释器,不能替代对静态状态、引用生命周期和第三方库调用的检查。

把以前由 GIL 暂时保护的状态找出来
free-threading 最容易低估的成本在这里:过去没有暴露的竞态,现在可能真的同时发生。扩展中的全局缓存、惰性初始化、引用计数周边操作、静态缓冲区和回调队列都要重新画出所有者与锁的边界。
一个常见的改造顺序是:先找共享可变状态,再决定是改成线程局部数据、使用明确的互斥锁,还是把对象所有权交给更高层。不要看到原子类型就认为整个操作序列已经安全,读取、检查、修改通常仍然需要一个一致性边界。
static PyMutex cache_mutex;
static PyObject *shared_cache;
static PyObject *get_cached_value(void)
{
PyObject *result;
/* 锁住“检查并取值”这一整个临界区,而不是只锁单次读取。 */
PyMutex_Lock(&cache_mutex);
result = shared_cache == NULL ? Py_None : shared_cache;
Py_INCREF(result);
PyMutex_Unlock(&cache_mutex);
return result;
}
上面的代码只表达锁边界示意,具体 API 和对象生命周期要以目标 CPython 版本及项目采用的线程原语为准。文章中的静态图也只用于解释关系,不表示这段示意代码已经在本机运行。
用双解释器矩阵决定是否可以发布
我会把 CI 至少拆成两条线:默认 GIL 构建和 free-threaded 构建。前者保证原有用户不受影响,后者负责发现轮子、初始化声明和竞态问题。测试命令还要显式确认 GIL 状态,否则某个不兼容依赖可能让后面的测试悄悄退回到带 GIL 模式。
strategy:
matrix:
# 两条解释器线都要跑同一组核心测试。
python-build: ["3.14", "3.14t"]
steps:
- name: 安装依赖
run: python -m pip install -e .
- name: 确认运行时状态
run: python -c "import sys; print(sys._is_gil_enabled())"
- name: 执行并发测试
run: python -m pytest -q tests/concurrency
发布前我会把结果分成三个等级:
- 可试用:有匹配的 free-threaded wheel,导入后 GIL 仍关闭,但只覆盖项目自己的基础测试。
- 可预发布:直接依赖和关键间接依赖都有兼容记录,扩展声明已完成,双解释器 CI 持续通过。
- 可生产:还要有目标平台 wheel、并发压测或业务回归、故障回退策略,以及对生态跟踪变化的维护责任。
如果项目使用 CPython 3.15,还可以进一步评估 abi3t。这是 free-threaded Stable ABI 的新方向,但它有自己的 API 限制和版本边界,不能把普通 abi3 wheel 直接当成 abi3t。
我的兼容清单怎么落地
现在我会为每个原生依赖保存下面这些字段:依赖版本、扩展来源、目标解释器、wheel 标签、模块声明方式、已知线程安全限制、测试平台、最后一次验证的提交。这样升级一个包时,变更的是一行清单,而不是重新凭感觉跑一次服务。
| 检查层 | 通过条件 | 未通过时的动作 |
|---|---|---|
| 解释器 | Py_GIL_DISABLED=1 且运行时 GIL 状态符合预期 | 先修复构建或启动参数,不进入性能比较 |
| 分发 | 安装到目标平台的 wheel 与 free-threaded ABI 匹配 | 升级依赖、补 wheel 或暂时回到普通构建 |
| 模块 | 初始化声明、封装层开关和构建日志一致 | 按初始化方式补声明,不能只改 Python 代码 |
| 并发 | 共享状态有明确所有权,双解释器 CI 和回归测试持续通过 | 补锁、改数据结构或缩小 free-threaded 支持范围 |
我最后的判断很简单:如果只是想做一次本地实验,先从依赖少、纯 Python 比例高的服务开始;如果要把 free-threading 当成生产能力,就把“轮子可安装”和“扩展可并发”分成两个发布门槛。前者解决交付,后者才解决运行时风险。
相关问题
导入一个旧扩展后 GIL 又打开了,说明什么?
通常说明扩展没有声明 free-threaded 兼容,或者依赖链中的其他扩展触发了回退。先记录导入警告,再逐个核对 wheel、模块初始化方式和运行时 GIL 状态。
纯 Python 包还需要做兼容性测试吗?
需要。它可能不需要重新编译,但原来被 GIL 偶然串行化的共享状态、回调顺序和第三方对象使用方式仍然可能暴露竞态。
有 cp314t wheel 就能直接上线吗?
不能。wheel 标签只说明分发产物面向该解释器 ABI;模块声明、线程安全实现、业务回归和目标平台覆盖仍然要单独通过。
runtime.SetFinalizer 与对象保活关系的判断
- 上一篇
- runtime.SetFinalizer 与对象保活关系的判断
- 下一篇
- crypto/hpke 封装密钥与上下文复用的边界
-
- 文章 · python教程 | 2小时前 | 序列化 · python · Python pickle 进程池 multiprocessing Pool
- Python multiprocessing 进程池传递不可序列化对象
- 255浏览 收藏
-
- 文章 · python教程 | 3小时前 | 数据一致性 · Python教程 · Python 事务 自动提交 sqlite3 autocommit isolation_level
- Python sqlite3 事务模式与自动提交边界
- 197浏览 收藏
-
- 文章 · python教程 | 3小时前 |
- Python contextlib.nullcontext 统一同步异步入口
- 316浏览 收藏
-
- 文章 · python教程 | 4小时前 | 面向对象 · python · Python教程 · InitVar __post_init__ Python dataclass 派生字段 field(init=False)
- Python dataclass __post_init__ 计算派生字段
- 303浏览 收藏
-
- 文章 · python教程 | 5小时前 |
- Python typing.TypeGuard 处理复杂容器类型收窄
- 437浏览 收藏
-
- 文章 · python教程 | 7小时前 |
- Python os.fspath 支持自定义路径对象
- 214浏览 收藏
-
- 文章 · python教程 | 9小时前 | 异常处理 · 异步编程 · Python教程 · asyncio · 后台任务 任务取消 CancelledError Python asyncio asyncio.shield
- Python asyncio.shield 保护后台任务免受外层取消
- 407浏览 收藏
-
- 文章 · python教程 | 11小时前 |
- Python configparser ExtendedInterpolation 组织分层配置
- 480浏览 收藏
-
- 文章 · python教程 | 22小时前 | python · Python tomllib TOMLDecodeError
- Python tomllib 解析失败时如何定位具体键与行列
- 198浏览 收藏
-
- 文章 · python教程 | 1天前 |
- Python contextvars 为什么能隔离并发请求上下文
- 202浏览 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 485次学习
-
- PubMedQA
- 深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
- 408次使用
-
- H2O EvalGPT
- H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
- 486次使用
-
- LMArena
- LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
- 494次使用
-
- HELM
- 深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
- 443次使用
-
- MMBench
- MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
- 270次使用
-
- go格式“占位符”输入输出 类似python的input
- 2023-01-19 346浏览
-
- Golang如何调用Python代码详解
- 2023-01-07 235浏览
-
- Go 1.26 的 go fix 怎么安全现代化旧代码:new(expr)、模块版本与回滚核对
- 2026-07-27 388浏览
-
- Go 1.24 泛型类型别名怎么落地:迁移旧 API 时的兼容边界
- 2026-07-27 335浏览
-
- Go 1.25 容器里的 GOMAXPROCS 怎么迁移:cgroup CPU 限额、自动更新与旧环境兼容
- 2026-08-09 438浏览

