ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

CPython 推迟 frozendict 的 GC 追踪到构造完成:gh-issue-151722 的实现原理与工程价值

2026/9/10 0:16:42 拓冰建站 浏览量
CPython 推迟 frozendict 的 GC 追踪到构造完成:gh-issue-151722 的实现原理与工程价值 CPython 推迟 frozendict 的 GC 追踪到构造完成gh-issue-151722 的实现原理与工程价值【免费下载链接】cpythonThe Python programming language项目地址: https://gitcode.com/GitHub_Trending/cp/cpython导读frozendict是 CPython 提供的不可变字典类型其对象一旦构造完成便不可修改因此非常适合作为配置快照、缓存键与跨线程共享数据。CPython 通过 Defer GC tracking offrozendictto end of constructiongh-issue-151722这项变更将frozendict纳入 GC 追踪的时机从分配即追踪推迟到构造完成之后避免半成品对象被并发线程通过gc模块发现与访问。本文以 Misc/NEWS.d/next/Core_and_Builtins/2026-06-20-00-30-47.gh-issue-151722.RPMPIY.rst 为线索深入 Objects/dictobject.c 源码与 Lib/test/test_dict.py 测试用例讲解该机制的实现细节、相关辅助函数以及可验证的运行效果。背景frozendict 的对象模型与不可变语义frozendict与dict共用同一套哈希表存储内核。从 Include/internal/pycore_dict.h 与 Include/cpython/dictobject.h 的声明可见PyFrozenDictObject与PyDictObject共享ma_keys/ma_values字段布局Objects/dictobject.c 中通过 clinic 同时声明了dict与frozendict两个类两者走同一条anydict分配路径。frozendict的不可变性体现在三个方面禁止修改操作赋值、删除、setdefault、pop、update、clear等操作统一抛出TypeError错误消息由frozendict_does_not_support(WHAT)宏生成Objects/dictobject.c例如 frozendict object does not support assignment。可哈希frozendict实现了frozendict_hashObjects/dictobject.c按(key, value)对的哈希逐项异或合并并缓存到ma_hash字段与frozenset的哈希策略同源。支持|合并frozendict_orObjects/dictobject.c保证frozendict | frozendict的结果仍为frozendict。由于对象不可变构造阶段是唯一可能内部状态不一致的窗口这正是 gh-issue-151722 优化所针对的场景。变更动机构造期的可见性窗口传统字典对象如dict在分配后立即调用_PyObject_GC_TRACK标记为 GC 可追踪见dict_newObjects/dictobject.c随后在tp_init中填充内容。dict之所以可以这样做是因为它天生可变即使被其他线程通过gc模块观察到也可以加锁或等待构造完成后再使用。frozendict的情况则不同。其构造路径frozendict_vectorcallObjects/dictobject.c与frozendict_newObjects/dictobject.c需要分配空对象从位置参数一个 mapping 或 iterable执行dict_update_arg/dict_update_common填充处理关键字参数逐个_PyAnyDict_SetItem全部填充成功后才对对象加哈希、标记为最终形态。如果在第 1 步就立即_PyObject_GC_TRACK那么从分配完成到填充完成之间对象就是一个处于半构造状态、且已被 GC 登记的frozendict。在 free-threadedPy_GIL_DISABLED构建或启用 GC 守护线程的环境下另一线程完全可以通过gc.get_objects()拿到这个引用并尝试读取——而此刻对象可能只填充了一半甚至尚未完成哈希缓存初始化。源码中的注释精确描述了这一动机Objects/dictobject.c/* gh-151722: Keep the frozendict untracked until it is fully built, so a half-built object is never reachable from another thread (using the gc module). */这正是本 NEWS 条目标题 Defer GC tracking of frozendict to end of construction 的核心含义。实现机制untracked 分配 构造完成后统一 TRACK整个机制由三个互相配合的部分组成。1. 分配阶段frozendict_new_untrackedfrozendict_new_untrackedObjects/dictobject.c负责分配一个不参与 GC 追踪的frozendict/* Allocate an empty, GC-untracked frozendict; the constructor tracks it once fully built. */ static PyObject * frozendict_new_untracked(PyTypeObject *type) { assert(PyObject_IsSubclass((PyObject*)type, (PyObject*)PyFrozenDict_Type)); PyObject *d anydict_new_untracked(type); if (d NULL) { return NULL; } assert(can_modify_dict(_PyAnyDict_CAST(d))); _PyFrozenDictObject_CAST(d)-ma_hash -1; return d; }关键点有两个底层调用anydict_new_untrackedObjects/dictobject.c它通过_PyType_AllocNoTrack分配对象因此不会立即把对象登记到 GC 追踪列表同时把ma_keys初始化为共享的Py_EMPTY_KEYS、ma_values置为NULL在返回前把ma_hash显式初始化为-1作为尚未计算哈希的哨兵值frozendict_hash依赖该哨兵做缓存判断。2. 构造阶段填充期间禁止被 GC 观察在frozendict_vectorcall与_PyDict_FromKeys等填充路径中对象始终保持 untracked 状态直到最后一个键值对插入完毕。调试断言can_modify_dictObjects/dictobject.c为此提供了强约束if (PyFrozenDict_Check(mp)) { // gh-151722: A frozendict must not be tracked by the GC // when its being modified. assert(!_PyObject_GC_IS_TRACKED(mp)); // No locking required to modify a newly created frozendict // since its only accessible from the current thread. assert(PyUnstable_Object_IsUniquelyReferenced(_PyObject_CAST(mp))); }这段断言揭示了两个设计收益GC 安全修改中的frozendict绝不处于 GC 追踪状态其他线程无法通过gc模块触达半成品免锁由于对象尚未对任何其他线程可见唯一引用填充过程无需加锁frozendict构造天然无竞争这与Py_GIL_DISABLED构建下普通dict需要关键段critical section保护形成对比。3. 收尾阶段_PyObject_GC_TRACK构造成功的最后一步统一调用_PyObject_GC_TRACK(self)完成登记frozendict_vectorcall在填充完位置参数与关键字参数后执行Objects/dictobject.cfrozendict_new在dict_update_common成功之后执行Objects/dictobject.cfrozendict_copy_impl对复制出的新对象同样遵循 untracked 复制 → TRACK 的模式Objects/dictobject.c。这样完整对象从出生起就以最终形态对外可见不存在半构造状态被暴露的窗口。相关路径的同类处理fromkeys 与子类构造该机制不仅覆盖常规构造还覆盖了两条容易被忽略的路径。_PyDict_FromKeys的 frozendict 分支dict.fromkeys对frozendict的调用由_PyDict_FromKeys处理Objects/dictobject.c。当cls恰好是PyFrozenDict_Type时直接改用frozendict_new_untracked分配而当调用的是重写了构造函数的 frozendict 子类此时子类构造器可能返回一个已被追踪的对象源码采用复制到 untracked 副本再继续填充的策略// gh-151722: If cls constructor returns a frozendict which is tracked by // the GC, create a frozendict copy which is not tracked by the GC. if (PyFrozenDict_Check(d) _PyObject_GC_IS_TRACKED(d)) { need_copy 1; PyObject *copy frozendict_new_untracked(PyFrozenDict_Type); ... if (dict_merge(copy, d, 1, NULL) 0) { Py_DECREF(copy); goto Fail; } Py_SETREF(d, copy); }注释解释了为什么不直接 untrack 原对象Untracking the frozendict requires tracking again the frozendict on error which is more complicated. Its easier to work on a copy.——即出错回滚时重新追踪的复杂度高于直接做一个 untracked 副本的成本。copy与 C API 入口frozendict.copy()frozendict_copy_impl对精确类型直接返回自身Py_NewRef对子类则走anydict_copy_untracked复制再统一 TRACKC API 入口PyFrozenDict_NewObjects/dictobject.c同样复用frozendict_new保证内部构造路径与公开 API 行为一致并实现PyFrozenDict_New(frozendict)原样返回的优化。测试与验证Lib/test/test_dict.py 中设有独立的FrozenDictTest基类CommonTest见 Lib/test/test_dict.py覆盖了frozendict(a1, b2)构造及__init__无副作用frozendict.__init__()不改变已构造对象frozendict(d)构造时避免复制直接复用精确类型对象|合并运算结果类型与哈希正确性hash(fd | frozendict()) hash(frozendict(...))Lib/test/test_dict.py各类不可变约束禁止赋值、删除等操作抛TypeError。对 GC 追踪语义本身Lib/test/test_dict.py 的test_copy_maintains_tracking验证复制操作保持追踪状态一致更广泛的 GC 行为由Lib/test/test_gc.py本仓库测试目录 Lib/test中针对dict/frozendict的追踪断言覆盖。读者可结合这些用例验证构造完成前对象不被gc.is_tracked捕获构造完成后才进入 GC 视野。工程价值与启发可见性即正确性对不可变类型而言构造完成才是合法的对外暴露时刻。将 GC 登记推迟到构造末尾从根本上消除了半成品对象被并发观察的可能而不是依赖锁去事后补救。免锁构造因为对象在填充期间只有当前线程可见构造路径无需任何临界区这对 free-threaded无 GIL构建的性能尤为重要。可复用的模式untracked 分配 → 填充 → 统一 TRACK的模式在 Objects/dictobject.c 中被frozendict_new、frozendict_vectorcall、_PyDict_FromKeys、copy四条路径一致遵循形成清晰的内部约定调试断言can_modify_dict把这一约定固化为运行时不变量防止未来代码改动破坏。小结gh-issue-151722 以一处看似微小的时序调整把_PyObject_GC_TRACK从分配后挪到构造完成后同时换来了 GC 线程安全与构造免锁两项收益。理解这一变更有助于开发者把握 CPython 在 free-threaded 时代对对象可见性的精细化控制思路也为实现自定义不可变容器提供了可借鉴的工程范式。【免费下载链接】cpythonThe Python programming language项目地址: https://gitcode.com/GitHub_Trending/cp/cpython创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考