CPython 自由线程构建 QSBR 槽位泄漏修复解析:从 gh-issue-155363 看线程状态创建失败路径的回收机制
CPython 自由线程构建 QSBR 槽位泄漏修复解析从 gh-issue-155363 看线程状态创建失败路径的回收机制【免费下载链接】cpythonThe Python programming language项目地址: https://gitcode.com/GitHub_Trending/cp/cpython导读本文围绕 CPython 仓库中一条 NEWS 变更记录Misc/NEWS.d/next/Core_and_Builtins/2026-08-07-13-40-12.gh-issue-155363.Qk3Vt9.rst展开在**自由线程构建free-threaded build即Py_GIL_DISABLED配置**下当线程状态PyThreadState创建失败、而内部 QSBRQuiescent-State Based Reclamation静默状态内存回收槽位已经预留给该线程时旧实现会泄漏该槽位导致 QSBR 数组在反复失败的场景下无界增长。读完本文你将掌握 QSBR 槽位的生命周期预留 → 注册 → 注销、失败路径为何会泄漏、修复的准确位置与方式以及如何通过源码验证该修复。一、变更记录原文与问题定位1.1 变更记录全文该 NEWS 条目完整内容如下Fix a leak in thefree-threaded buildwhen creating a thread state fails after an internal QSBR slot has been reserved for it. The slot could never be reclaimed, so the QSBR array grew without bound across repeated failures.用中文表述即修复自由线程构建中的一个泄漏——当线程状态创建失败、且在此之前已经为它预留了内部 QSBR 槽位时该槽位永远无法被回收导致 QSBR 数组在反复失败的过程中无界增长。这条记录虽然只有三句话但信息密度很高涉及三个关键概念free-threaded build自由线程构建CPython 的--disable-gil实验性构建模式由Py_GIL_DISABLED宏控制QSBR为自由线程构建引入的安全内存回收SMR机制用于在存在并发读访问时安全地延迟释放共享内存QSBR 槽位slot每个线程在进入解释器时需要在 per-interpreter 的 QSBR 状态数组中占用一个条目。1.2 为什么说这是一条值得深挖的变更表面上看它只是一条修了个泄漏的新闻但结合源码可以发现它触及了自由线程构建中一个极易发生、却极难观测的资源生命周期问题——QSBR 数组由PyMem_RawCalloc按需扩容分配数组大小由shared-size记录一旦某个槽位被置为allocated状态却没有任何线程引用它该槽位既不会被扫描poll回收也不会进入空闲链表freelist数组只能不断扩容。若创建线程状态的操作如内存不足、TLBC 索引耗尽反复失败泄漏会线性累积最终造成不可忽视的内存膨胀。二、QSBR 机制速览为什么需要槽位在深入修复细节之前先建立对 QSBR 的基础认知。CPython 在自由线程构建中许多无锁数据结构如字典键表PyDictKeysObject、列表后备数组_PyListArray在读与写并发时不能立刻释放被替换的旧内存否则读线程可能踩到 use-after-free。QSBR 的做法是让每个线程定期在静默点报告自己的读取序列号只有当一个对象的目标序列号qsbr_goal小于等于所有存活线程的最小读取序列号时才能安全释放它。全局写序列wr_seqper-interpreter 计数器从 1 开始、每次递增 2始终保持奇数见 Include/internal/pycore_qsbr.h 中的QSBR_OFFLINE 0、QSBR_INITIAL 1、QSBR_INCR 2定义每线程读序列线程到达静默状态时把当前wr_seq拷贝到本地_Py_qsbr_quiescent_stateInclude/internal/pycore_qsbr.h全局读序列rd_seq所有在线线程读序列的最小值由qsbr_poll_scan()扫描线程状态数组得出Python/qsbr.c。仓库中还有一份专门的设计文档 InternalDocs/qsbr.md详细说明了 QSBR 在自由线程构建中的用途、实现细节与已知限制例如扫描所有线程状态在超过约 1000 个线程的应用中可能成为瓶颈。而每个线程要参与 QSBR就必须在 per-interpreter 的 QSBR 状态数组中占有一个_qsbr_thread_state条目这个条目就是本次 NEWS 所说的slot。相关数据结构定义在 Include/internal/pycore_qsbr.hstruct _qsbr_thread_state每线程状态含seq最后观测的写序列0 表示离线、deferred_count、deferred_memory、deferred_page_memory、should_process、allocated、freelist_next等字段struct _qsbr_sharedper-interpreter 共享状态含wr_seq、rd_seq、array64 字节对齐的_qsbr_pad数组、size、以及受PyMutex保护的freelist。三、槽位生命周期预留、注册与注销QSBR 槽位与PyThreadState的生命周期严格绑定对应的 API 在 Include/internal/pycore_qsbr.h 中声明实现位于 Python/qsbr.c。3.1 槽位预留_Py_qsbr_reserve()当创建一个新的PyThreadState时CPython 会先为它预留一个 QSBR 槽位。入口在 Python/pystate.c 的new_threadstate()#ifdef Py_GIL_DISABLED int32_t tlbc_idx _Py_ReserveTLBCIndex(interp); if (tlbc_idx 0) { free_threadstate(tstate); return NULL; } Py_ssize_t qsbr_idx _Py_qsbr_reserve(interp); if (qsbr_idx 0) { _Py_UnreserveTLBCIndex(interp, tlbc_idx); free_threadstate(tstate); return NULL; } #endif注意两点先预留 TLBC 索引再预留 QSBR 槽位。TLBCThread-Local ByteCode是自由线程构建中每个线程独立的字节码副本索引其预留/释放 API 声明在 Include/internal/pycore_code.h_Py_ReserveTLBCIndex/_Py_UnreserveTLBCIndex。当 QSBR 预留失败时代码已经正确地回滚了 TLBC 索引——这说明创建失败时的资源回滚在本修复之前就是代码库的既有约定_Py_qsbr_reserve()返回的是数组下标而非指针注释明确说明原因是数组可能在后续扩容时被重新分配指针会失效Python/qsbr.c。_Py_qsbr_reserve()的实现Python/qsbr.c优先从共享freelist取一个空闲槽位若 freelist 为空则调用grow_thread_array()将数组翻倍扩容初始最小为 8 个条目见MIN_ARRAY_SIZE定义 Python/qsbr.c扩容需要先_PyEval_StopTheWorld()暂停所有线程再把旧数组内容memcpy到新数组并重建 freelistPython/qsbr.c。3.2 槽位注册_Py_qsbr_register()new_threadstate()在完成init_threadstate()并把线程加入解释器的线程链表后才调用_Py_qsbr_register()把槽位与线程状态关联起来Python/pystate.c#ifdef Py_GIL_DISABLED // Must be called with lock unlocked to avoid lock ordering deadlocks. _Py_qsbr_register(tstate, interp, qsbr_idx); tstate-tlbc_index tlbc_idx; #endif_Py_qsbr_register()Python/qsbr.c在互斥锁保护下完成qsbr-tstate tstate; tstate-qsbr qsbr;的双向关联。3.3 槽位注销_Py_qsbr_unregister()线程状态销毁时调用_Py_qsbr_unregister()Python/pystate.c其实现Python/qsbr.c把allocated置回false、清空tstate指针并把槽位压回freelist供后续线程复用。注意freelist是槽位复用池与数组扩容是两个层面只要槽位能回到 freelist数组就不会因为反复创建/销毁线程而无界增长。四、泄漏根因预留成功但注册永远不发生把上面的生命周期拼起来就能精确复现 NEWS 中所说的泄漏new_threadstate()调用_Py_qsbr_reserve()成功从 freelist 取走一个槽位并将其allocated true随后HEAD_LOCK→init_threadstate()→add_threadstate()之间的任意一步失败例如init_threadstate()内部的资源分配失败函数在HEAD_UNLOCK之前提前返回NULL此时没有任何代码把槽位还给 freelist——_Py_qsbr_unregister()只会在线程状态正常销毁路径上被调用而这里PyThreadState本身已经就地释放free_threadstate()Python/pystate.c只是归还给 preallocated 池或直接PyMem_RawFree该槽位保持allocated true且tstate NULL的幽灵状态既不会被任何线程使用也永远无法回到 freelist。从源码结构看修复前new_threadstate()的失败路径只回滚了 TLBC 索引_Py_UnreserveTLBCIndex没有对 QSBR 槽位做任何回收动作。这正好与 NEWS 的描述一一对应创建线程状态失败后已预留的 QSBR 槽位永远无法回收QSBR 数组在反复失败中无界增长。泄漏的实际影响随失败频率线性放大grow_thread_array()每次扩容都会把size翻倍并calloc一块更大的数组虽然旧数组会被PyMem_RawFree释放但共享结构shared-size只会单调增长如果创建线程状态这个操作被反复触发且反复失败例如内存压力下的线程风暴size会一路膨胀扫描qsbr_poll_scan成本也随之上升。五、修复方式与验证路径5.1 修复思路修复方案非常直接在new_threadstate()的 QSBR 预留失败分支之外补上预留成功但后续创建失败时的回滚调用。即把_Py_qsbr_reserve()返回的qsbr_idx在错误路径上归还。仓库中与之语义对称的既有先例是_Py_UnreserveTLBCIndex()注释明确说明它是释放一个由_Py_ReserveTLBCIndex()预留但从未存入PyThreadState的索引Include/internal/pycore_code.h说明预留后回滚是自由线程构建中既定的资源管理模式修复后的new_threadstate()失败路径应当形如先_Py_UnreserveTLBCIndex(interp, tlbc_idx)再归还 QSBR 槽位使其回到 freelist最后free_threadstate(tstate)保证三个预留资源全部对称回收。可以推断修复涉及新增一个与_Py_UnreserveTLBCIndex对应的 QSBR 回滚 API或在_Py_qsbr_register前置条件下复用注销逻辑并在new_threadstate()的失败路径调用它。这条变更归类于Core_and_Builtins说明它落在核心运行时解释器/线程状态管理而非某个具体模块。5.2 如何在仓库中验证修复阅读关联文档Misc/NEWS.d/next/Core_and_Builtins/2026-08-07-13-40-12.gh-issue-155363.Qk3Vt9.rst 本身核对创建路径检查 Python/pystate.c 中new_threadstate()的所有失败分支是否都已对称回滚 TLBC 与 QSBR 资源核对槽位回收语义在 Python/qsbr.c 中确认_Py_qsbr_reserve()与_Py_qsbr_unregister()对allocated/freelist的维护是否闭合关注后续演进qsbr_poll_scan()需要遍历整个数组Python/qsbr.c数组规模失控不仅浪费内存也会拖慢每次 QSBR poll这解释了为何无界增长被当作必须修复的缺陷。六、从修复看自由线程构建的资源管理原则这条变更虽小却浓缩了自由线程构建Py_GIL_DISABLED中资源管理的一条核心原则凡是 per-thread 的共享资源预留reserve与回收release/unreserve必须成对出现且错误路径必须与正常路径同样严谨。类似的成对资源在仓库中还有不少可以作为延伸阅读资源预留回收/注销关键位置QSBR 槽位_Py_qsbr_reserve()_Py_qsbr_unregister()Python/qsbr.cTLBC 索引_Py_ReserveTLBCIndex()_Py_UnreserveTLBCIndex()/_Py_ClearTLBCIndex()Include/internal/pycore_code.hQSBR 写序列推进_Py_qsbr_advance()由qsbr_poll_scan()驱动的延迟释放Python/qsbr.c而 QSBR 本身的设计权衡延迟推进写序列以减少内存竞争、以QSBR_DEFERRED_LIMIT/QSBR_FREE_MEM_LIMIT等阈值约束峰值内存、eval_breaker作为静默点等都可以在 InternalDocs/qsbr.md 与 Objects/obmalloc.c 的延迟释放实现中进一步研读。结语gh-issue-155363 对应的这条 NEWS 记录揭示了自由线程构建中一个隐藏很深、后果很实的资源泄漏一次PyThreadState创建失败可能让一个 QSBR 槽位永久脱离回收路径并在反复失败下让 QSBR 数组无界膨胀。修复的本质是让预留与回收在错误路径上同样对称。对自由线程构建的维护者与研究者而言理解reserve → register → unregister的完整链路是排查这类问题的第一手方法论。【免费下载链接】cpythonThe Python programming language项目地址: https://gitcode.com/GitHub_Trending/cp/cpython创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
上一篇/下一篇内容由系统自动关联
返回资讯列表 →