news 2026/9/10 3:21:58

types.GenericAlias 迭代器数据竞争修复:CPython 自由线程(free-threaded)构建下的并发安全剖析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
types.GenericAlias 迭代器数据竞争修复:CPython 自由线程(free-threaded)构建下的并发安全剖析

types.GenericAlias 迭代器数据竞争修复:CPython 自由线程(free-threaded)构建下的并发安全剖析

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

导读

本篇文章围绕 CPython 最新 NEWS 条目Misc/NEWS.d/next/Core_and_Builtins/2026-07-19-19-14-39.gh-issue-154043.Kych7F.rst展开,深入剖析一个针对types.GenericAlias(即list[int]tuple[int, str]这类泛型别名对象)迭代器的数据竞争(data race)修复。你将了解到:在自由线程(free-threaded,即禁用 GIL)构建下,多个线程共享迭代一个GenericAlias时为什么会发生竞争;修复采用的关键机制——critical section(临界区)在 Objects/genericaliasobject.c 中如何落地;以及仓库内对应的测试如何验证迭代器的单次产出与StopIteration语义。读完本文,你能从源码层面理解 CPython 在 free-threaded 模式下为对象状态加锁的通用范式,并能定位到本次修复涉及的具体文件与代码路径。

一、新闻条目原文与问题本质

本次修复对应的 NEWS 条目全文如下:

Fix a data race when iterating a sharedtypes.GenericAliasiterator from multiple threads under thefree-threaded build.

翻译过来即:修复在自由线程构建下,多个线程共享迭代同一个types.GenericAlias迭代器时产生的数据竞争

分解这条信息,可以提取出四个关键要素:

要素含义
涉及对象types.GenericAlias(如list[int]tuple[int, str]
触发场景多个线程共享同一个迭代器对象并同时调用next()
构建模式free-threaded build(--disable-gil编译出的无 GIL 解释器)
问题类型数据竞争(data race),属于未定义行为,可能导致崩溃或错误结果

1.1 为什么 free-threaded 构建下会出现数据竞争

在传统 CPython 构建中,全局解释器锁(GIL)保证了同一时刻只有一个线程执行 Python 字节码,对象字段的读写天然串行化。而在 free-threaded 构建(Py_GIL_DISABLED)中,多个线程可以真正并行执行,任何没有同步保护的对象字段读写都会构成数据竞争。

GenericAlias的迭代器对象gaiterobject在迭代过程中需要读取并清空其内部保存的引用gi->obj,以实现在第一次next()返回“带星号的别名”(starred alias)、第二次next()抛出StopIteration的单次消费语义。在无锁并行场景下,两个线程同时执行这一“读-清-空”操作就会竞争同一字段,这正是本次修复要消除的竞争点。

二、修复前的隐患:迭代器状态的读改写

GenericAlias的迭代器实现在 Objects/genericaliasobject.c 中。首先看迭代器对象的结构与创建逻辑:

static PyObject * ga_iter(PyObject *self) { gaiterobject *gi = PyObject_GC_New(gaiterobject, &_Py_GenericAliasIterType); if (gi == NULL) { return NULL; } gi->obj = Py_NewRef(self); // 持有被迭代的 GenericAlias 对象 PyObject_GC_Track(gi); return (PyObject *)gi; }

迭代器类型_Py_GenericAliasIterType(源码中注释gh-91632: _Py_GenericAliasIterType is exported to be cleared in _PyTypes_FiniTypes)的定义如下:

PyTypeObject _Py_GenericAliasIterType = { PyVarObject_HEAD_INIT(&PyType_Type, 0) .tp_name = "generic_alias_iterator", .tp_basicsize = sizeof(gaiterobject), .tp_iter = PyObject_SelfIter, .tp_iternext = ga_iternext, .tp_traverse = ga_iter_traverse, .tp_methods = ga_iter_methods, .tp_dealloc = ga_iter_dealloc, .tp_clear = ga_iter_clear, .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_GC, };

迭代的核心是tp_iternext指向的ga_iternext。修复后的实现如下:

static PyObject * ga_iternext(PyObject *op) { gaiterobject *gi = (gaiterobject*)op; PyObject *obj; Py_BEGIN_CRITICAL_SECTION(gi); obj = gi->obj; gi->obj = NULL; // 一次性消费:取出后立即置空 Py_END_CRITICAL_SECTION(); if (obj == NULL) { PyErr_SetNone(PyExc_StopIteration); return NULL; } gaobject *alias = (gaobject *)obj; PyObject *starred_alias = Py_GenericAlias(alias->origin, alias->args); Py_DECREF(obj); if (starred_alias == NULL) { return NULL; } ((gaobject *)starred_alias)->starred = true; return starred_alias; }

修复的核心手法一目了然:Py_BEGIN_CRITICAL_SECTION(gi)/Py_END_CRITICAL_SECTION()把“读取gi->obj并将其置空”这一对读改写操作包裹起来,构成原子临界区。

三、修复手段:critical section 在 free-threaded 下的作用原理

critical section 是 CPython 为 free-threaded 构建提供的一种对象级细粒度锁机制,相关宏定义位于 Include/internal/pycore_critical_section.h。

#ifdef Py_GIL_DISABLED ... #define Py_BEGIN_CRITICAL_SECTION(op) \ ... #define Py_END_CRITICAL_SECTION() ... #else /* !Py_GIL_DISABLED */ ... #endif /* !Py_GIL_DISABLED */

关键设计要点:

  • 仅在 free-threaded 构建(Py_GIL_DISABLED)下才有实际加锁行为;传统 GIL 构建下这些宏展开为空操作,不引入任何额外开销。这与 NEWS 条目中“under the free-threaded build”的限定完全一致。
  • 对简单对象使用基于对象头的自旋锁(spinlock),对复杂对象则通过对象携带的互斥量实现。gaiterobject属于简单对象场景,因此使用Py_BEGIN_CRITICAL_SECTION(gi)直接锁定迭代器对象本身。
  • 临界区的语义是:进入时获得锁,退出时释放锁,从而保证被保护字段的读改写不会被其他线程交错执行。

正是这一对宏,消除了多线程共享同一个GenericAlias迭代器时的数据竞争:任意时刻只有一个线程能执行“取出gi->obj并置空”,其余线程要么看到NULL并拿到StopIteration,要么等待锁释放后继续——迭代器的“单次消费”语义在并行环境下得以保持。

值得注意的是,同文件中另一处对gi->obj的访问——ga_iter_reduce(支持pickle序列化迭代器)也采用了同样的临界区保护:

static PyObject * ga_iter_reduce(PyObject *self, PyObject *Py_UNUSED(ignored)) { PyObject *iter = _PyEval_GetBuiltin(&_Py_ID(iter)); /* _PyEval_GetBuiltin can invoke arbitrary code, * call must be before access of iterator pointers. * see issue #101765 */ PyObject *obj; Py_BEGIN_CRITICAL_SECTION(gi); obj = Py_XNewRef(gi->obj); Py_END_CRITICAL_SECTION(); ... }

这里把对gi->obj的引用获取同样放进临界区,并在注释中强调:可能触发任意代码的内建函数调用必须放在访问迭代器指针之前,避免锁内执行任意 Python 代码。这种“先把可能跑任意代码的调用挪出锁、再在锁内做最简字段操作”的写法,是 CPython 编写临界区代码的通用纪律。

四、修复前后行为对比与测试佐证

4.1 语义不变的保证:测试用例验证

本次修复只改变并发安全性,不改变迭代器原有的单线程行为。仓库中的测试文件 Lib/test/test_genericalias.py 覆盖了迭代器的核心语义:

def test_iter_creates_starred_tuple(self): t = tuple[int, str] iter_t = iter(t) x = next(iter_t) self.assertEqual(repr(x), '*tuple[int, str]') def test_calling_next_twice_raises_stopiteration(self): t = tuple[int, str] iter_t = iter(t) next(iter_t) with self.assertRaises(StopIteration): next(iter_t) def test_del_iter(self): t = tuple[int, str] iter_x = iter(t) del iter_x
  • test_iter_creates_starred_tuple验证第一次next()返回一个repr*tuple[int, str]带星号别名(starred alias),这正是ga_iternext((gaobject *)starred_alias)->starred = true;的效果;
  • test_calling_next_twice_raises_stopiteration验证第二次next()抛出StopIteration,对应gi->obj已被置空后进入PyErr_SetNone(PyExc_StopIteration)分支;
  • test_del_iter验证迭代器析构(ga_iter_dealloc)路径正常。

此外,test_unpack验证了带星号别名的__unpacked__属性在元组解包场景下的表现:

def test_unpack(self): alias = tuple[int, ...] # 示例语义 self.assertIs(alias.__unpacked__, False) unpacked = (*alias,)[0] self.assertIs(unpacked.__unpacked__, True)

这些测试共同确认:修复前后迭代器的功能契约——单次产出、第二次即抛StopIteration、解包语义——完全不变,变动的只是并发安全属性。

4.2 共享迭代器的并发使用场景

在实际代码中,“共享同一个GenericAlias迭代器”通常出现在将迭代器对象传给多个线程或协程消费的场景,例如:

import types from concurrent.futures import ThreadPoolExecutor alias = tuple[int, str] shared_iter = iter(alias) # 多个线程共享同一个迭代器 def consume(): try: return next(shared_iter) except StopIteration: return None with ThreadPoolExecutor(max_workers=4) as pool: results = list(pool.map(lambda _: consume(), range(8)))

在修复前,多个线程并发执行next(shared_iter)时,gi->obj的读-清-空操作存在竞争,可能产生未定义行为(例如同一引用被多个线程重复获取后再DECREF,导致引用计数错乱乃至崩溃)。修复后,临界区保证一次只有一个线程完成状态转移,其余线程稳定获得StopIteration

五、定位与延伸:相关代码路径速查

关注点仓库路径说明
NEWS 条目Misc/NEWS.d/next/Core_and_Builtins/2026-07-19-19-14-39.gh-issue-154043.Kych7F.rst本次修复的变更记录
迭代器实现Objects/genericaliasobject.cga_iternextga_iterga_iter_reduce_Py_GenericAliasIterType
critical section 宏Include/internal/pycore_critical_section.hPy_BEGIN_CRITICAL_SECTIONPy_GIL_DISABLED下的定义
行为测试Lib/test/test_genericalias.pytest_iter_creates_starred_tupletest_calling_next_twice_raises_stopiterationtest_del_itertest_unpack
泛型别名对象整体Objects/genericaliasobject.cPy_GenericAliasTypetp_name = "types.GenericAlias")、Py_GenericAlias()工厂函数

六、小结

  • 本次 NEWS 条目记录的修复,针对 free-threaded 构建下共享types.GenericAlias迭代器时的数据竞争;
  • 竞争点位于迭代器内部字段gi->obj的“读取并置空”操作;修复以Py_BEGIN_CRITICAL_SECTION(gi)/Py_END_CRITICAL_SECTION()临界区包裹该操作,保证状态转移的原子性;
  • critical section 仅在Py_GIL_DISABLED的 free-threaded 构建中产生实际锁行为,传统构建零开销;
  • 修复不改变迭代器单次产出、二次StopIteration的既有语义,相关行为由 Lib/test/test_genericalias.py 中的测试持续守护。

对于希望深入理解 CPython free-threaded 并发模型的开发者而言,本修复是一个极佳的入门案例:它展示了如何用最小粒度的 critical section 保护对象内部状态,同时保持对传统构建的完全兼容。

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

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

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

MoE大模型显存不够?Megatron下专家权重CPU Offload实战指南

最近在折腾大规模MoE模型时,我几乎被显存问题整崩溃。单卡80GB看着很大,可一旦模型里挂了64个专家,光专家模块的权重就能把显存吃掉大半。Megatron这套框架在模型并行上确实做得很极致,但面对MoE的海量专家权重,它默认…

作者头像 李华
网站建设 2026/9/10 3:21:33

昇腾/GE SetAttr算子属性设置

SetAttr 【免费下载链接】ge GE(Graph Engine)是面向昇腾的图编译器和执行器,提供了计算图优化、多流并行、内存复用和模型下沉等技术手段,加速模型执行效率,减少模型内存占用。 GE 提供对 PyTorch、TensorFlow 前端的…

作者头像 李华
网站建设 2026/9/10 3:18:31

Refine 中的 React 18 升级指南:新特性、API 迁移与工程实践

Refine 中的 React 18 升级指南:新特性、API 迁移与工程实践 【免费下载链接】refine A React Framework for building internal tools, admin panels, dashboards & B2B apps with unmatched flexibility. 项目地址: https://gitcode.com/GitHub_Trending/re…

作者头像 李华
网站建设 2026/9/10 3:17:24

STM32 RS485通信实战:从硬件电路到HAL库代码

简介:面向STM32F103平台的RS485通信参考工程,适合嵌入式开发入门者、工业自动化及远程监控项目技术人员,重点解决长距离多节点串行通信中的UART配置、485驱动器控制与收发切换问题。压缩包含122个文件,以C源码、H头文件和启动汇编…

作者头像 李华
网站建设 2026/9/10 3:17:19

TMS320VC5509A上McBSP与DMA协同驱动实战指南

简介:本资源是面向嵌入式DSP开发者的TMS320VC5509A芯片DMA实战工程,聚焦McBSP外设与DMA协同工作的底层驱动实现,适用于通信、音频实时处理等对数据吞吐与时序敏感的应用场景,适合具备C语言基础和TI C55x架构初步认知的中级开发者学…

作者头像 李华