news 2026/9/11 22:29:07

CPython free-threaded 构建下的 dlopen 标志并发安全修复:sys.setdlopenflags/getdlopenflags 的原子化改造

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
CPython free-threaded 构建下的 dlopen 标志并发安全修复:sys.setdlopenflags/getdlopenflags 的原子化改造

CPython free-threaded 构建下的 dlopen 标志并发安全修复:sys.setdlopenflags/getdlopenflags 的原子化改造

【免费下载链接】cpythonThe Python programming language项目地址: https://gitcode.com/GitHub_Trending/cp/cpython

本文围绕 CPython 仓库中 Misc/NEWS.d/next/Core_and_Builtins/2026-06-18-00-00-00.gh-issue-151644.5cFffN.rst 记录的变更展开:在 free-threaded(--disable-gil)构建下,sys.setdlopenflagssys.getdlopenflags并发调用时存在数据竞争,本次修复将底层_PyImport_GetDLOpenFlags/_PyImport_SetDLOpenFlags改为原子 load/store 操作。读完本文,你将理解该数据竞争的成因、修复方案的设计取舍、从 Python API 到dlopen系统调用的完整调用链,以及 relaxed 原子语义在此场景下为何足够。

一、变更背景:一条 NEWS 条目背后的数据竞争

原始变更记录非常简短,核心信息如下:

Fix a data race insys.setdlopenflagsandsys.getdlopenflagswhen called concurrently in the free-threaded build. The underlying_PyImport_GetDLOpenFlagsand_PyImport_SetDLOpenFlagsfunctions now use atomic load/store operations.

它对应 gh-issue-151644,主题是自由线程构建下的数据竞争(data race)修复。要理解这条 NEWS 的含义,需要先厘清三个概念:

  1. free-threaded build:CPython 提供的禁用 GIL(全局解释器锁)的构建配置,允许 Python 线程真正并行执行。此模式下不再有 GIL 兜底保护解释器内部共享状态,任何非原子读写都可能构成 C 语言层面的数据竞争(未定义行为)。
  2. dlopen flags:传给 POSIXdlopen()的动态库加载标志,决定符号解析策略(RTLD_LAZY/RTLD_NOW)与符号可见性(RTLD_GLOBAL/RTLD_LOCAL)等。
  3. 数据竞争:多个线程同时访问同一内存位置,且至少有一个是写操作、访问未经同步时的未定义行为,可能导致读到撕裂(torn)的值或编译器做出错误优化。

二、问题根源:解释器级共享状态在自由线程下的竞争

dlopen flags 并不是进程级全局变量,而是保存在**解释器状态(interpreter state)**中。在 Include/internal/pycore_interp_structs.h 可以看到int dlopenflags;字段,并通过 Include/internal/pycore_import.h 中的初始化宏设置默认值:

#ifdef HAVE_DLOPEN # include <dlfcn.h> // RTLD_NOW, RTLD_LAZY # if HAVE_DECL_RTLD_NOW # define _Py_DLOPEN_FLAGS RTLD_NOW # else # define _Py_DLOPEN_FLAGS RTLD_LAZY # endif # define DLOPENFLAGS_INIT .dlopenflags = _Py_DLOPEN_FLAGS,

即:默认标志为RTLD_NOW(若平台未声明该常量则回退到RTLD_LAZY),且整个实现被#ifdef HAVE_DLOPEN保护,仅存在于支持dlopen的平台。

竞争的具体场景是:

  • 写路径:任意 Python 线程调用sys.setdlopenflags(flags)修改解释器状态中的dlopenflags
  • 读路径:任意线程在导入扩展模块时,动态加载器读取dlopenflags并传给dlopen()。读取发生在 Python/dynload_shlib.c 的_PyImport_FindSharedFuncptr中:
dlopenflags = _PyImport_GetDLOpenFlags(_PyInterpreterState_GET()); handle = dlopen(pathname, dlopenflags);

在 free-threaded 构建下,同一解释器内的多个 Python 线程可以同时执行这两条路径——一个线程正在修改标志,另一个线程正在导入扩展模块读取标志。原先的非原子读写构成典型的数据竞争:读取线程可能观察到撕裂的中间值,且 C 标准将此类行为定义为未定义行为,编译器有权做出激进的重排或缓存优化。

三、修复实现:FT_ATOMIC 原子宏的引入

修复的核心改动位于 Python/import.c:

#ifdef HAVE_DLOPEN int _PyImport_GetDLOpenFlags(PyInterpreterState *interp) { return FT_ATOMIC_LOAD_INT_RELAXED(DLOPENFLAGS(interp)); } void _PyImport_SetDLOpenFlags(PyInterpreterState *interp, int new_val) { FT_ATOMIC_STORE_INT_RELAXED(DLOPENFLAGS(interp), new_val); } #endif // HAVE_DLOPEN

FT_ATOMIC_*前缀代表Free-Threaded Atomic,这些宏定义在 Include/internal/pycore_pyatomic_ft_wrappers.h:

#define FT_ATOMIC_STORE_INT_RELAXED(value, new_value) \ _Py_atomic_store_int_relaxed(&value, new_value) #define FT_ATOMIC_LOAD_INT_RELAXED(value) \ _Py_atomic_load_int_relaxed(&value)

该头文件的关键设计是条件编译双轨制:在 free-threaded 构建下,宏展开为真正的原子操作;在非自由线程(默认带 GIL)构建下,宏退化为普通赋值,零额外开销(见 pycore_pyatomic_ft_wrappers.h):

#define FT_ATOMIC_LOAD_INT_RELAXED(value) value #define FT_ATOMIC_STORE_INT_RELAXED(value, new_value) value = new_value

这意味着修复对默认构建完全没有性能影响,只在需要时(自由线程构建)付出原子操作的成本——这正是 CPython 内部并发基础设施的常见做法:宏封装、按构建模式切换。

四、完整调用链:从 sys 模块 API 到 dlopen

4.1 用户态入口:sys.setdlopenflags / getdlopenflags

两个公开 API 在 Python/sysmodule.c 中实现,同样被#ifdef HAVE_DLOPEN保护。它们由 Argument Clinic 生成签名与文档字符串(见 Python/clinic/sysmodule.c.h):

static PyObject * sys_setdlopenflags_impl(PyObject *module, int new_val) { PyInterpreterState *interp = _PyInterpreterState_GET(); _PyImport_SetDLOpenFlags(interp, new_val); Py_RETURN_NONE; } static PyObject * sys_getdlopenflags_impl(PyObject *module) { PyInterpreterState *interp = _PyInterpreterState_GET(); return PyLong_FromLong( _PyImport_GetDLOpenFlags(interp)); }

注意两点实现细节:

  • 两个函数都通过_PyInterpreterState_GET()获取当前线程所属的解释器,因为 flags 是 per-interpreter 状态;
  • setdlopenflags返回Nonegetdlopenflags把 int 包装为PyLong返回。

symtable、方法表描述(Python/sysmodule.c)以及sys模块顶层文档均登记了这两个函数。

4.2 中间层:导入子系统的内部接口

函数声明位于 Include/internal/pycore_import.h:

extern int _PyImport_GetDLOpenFlags(PyInterpreterState *interp); extern void _PyImport_SetDLOpenFlags(PyInterpreterState *interp, int new_val);

这是导入子系统的内部 API(pycore_前缀表明不对外部扩展公开),供sys模块(设置)与动态加载器(读取)两方使用。

4.3 底层消费方:扩展模块加载

真正消费 flags 的是 Python/dynload_shlib.c 的_PyImport_FindSharedFuncptr:该函数在导入共享库形式的扩展模块时被调用,先读取当前解释器的dlopenflags,再将其作为参数传给dlopen(pathname, dlopenflags)。因此,sys.setdlopenflags对后续所有扩展模块的dlopen调用立即生效。

4.4 典型使用方式

结合os模块的RTLD_xxx常量(os.RTLD_LAZYos.RTLD_NOWos.RTLD_GLOBALos.RTLD_LOCAL等),常见操作有:

import sys import os # 读取当前 dlopen 标志 flags = sys.getdlopenflags() print(flags) # 让扩展模块之间共享符号(例如让一个扩展导出的符号对另一个扩展可见) sys.setdlopenflags(flags | os.RTLD_GLOBAL) # 使用 lazy 符号解析(提高加载速度,代价是运行期可能解析失败) sys.setdlopenflags(os.RTLD_LAZY) # 恢复默认值 RTLD_NOW sys.setdlopenflags(os.RTLD_NOW)

五、relaxed 原子语义为何足够

修复选用的是relaxed(宽松)内存序,而不是 acquire/release 或 seq_cst。从并发语义学角度可以推断其设计考量:

  • 问题的本质是原子性而非排序:此场景并发操作的是单个int标量。需要消除的是撕裂读写和 C 语言层面的数据竞争(未定义行为),并不需要跨线程的 happens-before 排序——flags 是自包含的值,读方不依赖写方其他内存操作的可见性;
  • relaxed 保证:读写不会被撕裂、不会构成数据竞争,但不提供顺序约束。这对 "读到旧值或新值都合法、但绝不读到中间态" 的需求完全够用;
  • 性能代价最小:relaxed 原子在大多数硬件上可编译为普通 load/store 指令(仅禁止编译器层面的破坏性优化),比 seq_cst(通常需要内存屏障)开销更低。

若未来某次调用需要"设置 flags 后再导入模块"的严格排序,则需升级为 release/acquire 语义;当前实现证明对 flags 这一标量状态,relaxed 是正确的默认选择。

六、跨编译器后端的原子操作实现

_Py_atomic_load_int_relaxed/_Py_atomic_store_int_relaxed是 CPython 原子抽象层的一部分,按编译器后端分文件实现:

  • Include/cpython/pyatomic.h 与 pyatomic.h:对外声明,供各后端实现;
  • Include/cpython/pyatomic_gcc.h 与 pyatomic_gcc.h:GCC/Clang 后端的__atomic内置函数实现;
  • Include/cpython/pyatomic_msc.h 与 pyatomic_msc.h:MSVC 后端实现;
  • Include/cpython/pyatomic_std.h 与 pyatomic_std.h:C11_Atomic标准实现。

这种分层让同一份 CPython 源码在 GCC、Clang、MSVC 上都能获得正确的原子语义,同时为未来接入更多编译器保留扩展点。pyatomic_std.h的存在也表明 CPython 正逐步向标准 C 原子模型收敛。

七、对使用者的影响与总结

从用户视角看,本次修复不改变任何 API 行为sys.setdlopenflags(flags)sys.getdlopenflags()的签名、语义、返回值均保持不变,RTLD_xxx常量依旧从os模块获取。变化仅发生在内部实现——两个底层函数从普通 int 读写改为原子 load/store。

它的价值集中在 free-threaded 构建(--disable-gil)下:

  • 正确性:消除了并发调用时的数据竞争未定义行为,getdlopenflags不再可能读到撕裂值;
  • 线程安全:允许"一个线程修改 flags、另一线程同时导入扩展模块"的安全并发;
  • 零成本兼容:默认(带 GIL)构建下宏退化为普通赋值,无任何性能回退。

这一改动也体现了 CPython 自由线程化进程中的通用模式:对解释器内部的标量共享状态,通过FT_ATOMIC_*宏族(定义于 pycore_pyatomic_ft_wrappers.h)按构建模式切换原子与非原子访问,既保证并发正确性,又不牺牲传统构建的性能。对于在 free-threaded 构建下编写多线程 Python 代码的开发者,这意味着动态链接标志的读写已具备语言运行时层面的安全保证,可以放心地在多线程环境中使用sys.setdlopenflags调整扩展模块的加载行为。

【免费下载链接】cpythonThe Python programming language项目地址: https://gitcode.com/GitHub_Trending/cp/cpython

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/11 22:27:13

如何用 mise deps 在输入变化时自动运行 npm、pip 等依赖安装器?

如何用 mise deps 在输入变化时自动运行 npm、pip 等依赖安装器&#xff1f; 【免费下载链接】mise dev tools, env vars, task runner 项目地址: https://gitcode.com/GitHub_Trending/mi/mise mise deps 是 mise 提供的依赖管理功能&#xff08;目前标记为 experiment…

作者头像 李华
网站建设 2026/9/11 22:24:38

南京冷凝式壁挂炉维修服务,欧米到家专业检测节能采暖设备运行异常以及故障报警

文章简介南京冬季采暖需求较高&#xff0c;壁挂炉作为家庭供暖和生活热水的重要设备&#xff0c;长期使用后容易出现不点火、不供暖、热水忽冷忽热、故障代码报警、水压异常、漏水等问题。欧米到家专注南京壁挂炉维修服务&#xff0c;提供燃气壁挂炉、电壁挂炉、冷凝壁挂炉、采…

作者头像 李华
网站建设 2026/9/11 22:22:46

PyTorch面部表情识别实战:从CNN设计到ONNX部署

简介&#xff1a;本资源是一套面向深度学习初学者与进阶实践者的面部表情识别完整项目方案&#xff0c;基于PyTorch框架实现卷积神经网络&#xff08;CNN&#xff09;建模&#xff0c;覆盖数据预处理、模型训练、评估可视化及部署推理全流程&#xff0c;适用于课程设计、毕业设…

作者头像 李华
网站建设 2026/9/11 22:22:23

Android Jetpack Compose 状态管理浅析

掌握声明式UI的核心&#xff0c;构建高效、可维护的响应式应用在 Android Jetpack Compose 中&#xff0c;状态管理是构建响应式 UI 的核心基石。Compose 采用声明式编程范式&#xff0c;确立了 UI f(state) 这一根本原则——UI 是状态的函数&#xff0c;当状态变化时&#xf…

作者头像 李华
网站建设 2026/9/11 22:21:32

MISRA C:2025全面解读:C11/C17入轨与安全编码迁移实战

看到“MISRA C:2025”这几个字&#xff0c;我身边不少嵌入式工程师的第一反应是&#xff1a;2012版才刚用顺&#xff0c;怎么又来一个2025&#xff1f;别紧张。这个新版的标准不是要推翻你已经养成的安全编码习惯&#xff0c;而是把最近十年C语言生态的变化正式纳入约束框架&am…

作者头像 李华