
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),僅供參考