ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

CPython 无 GIL 构建下类型特殊方法并发赋值死锁的修复解析

CPython 无 GIL 构建下类型特殊方法并发赋值死锁的修复解析 CPython 无 GIL 构建下类型特殊方法并发赋值死锁的修复解析【免费下载链接】cpythonThe Python programming language项目地址: https://gitcode.com/GitHub_Trending/cp/cpython导读本文围绕 CPython 仓库中一条针对 free-threaded无 GIL构建的核心缺陷修复记录展开剖析两个线程同时对同一个类的特殊方法如__repr__、__hash__赋值时可能触发的死锁问题。文章将以 Misc/NEWS.d/next/Core_and_Builtins/2026-08-07-18-32-15.gh-issue-155400.Kq3Vpt.rst 为骨架结合 Objects/typeobject.c 的源码实现还原缺陷成因、修复策略及其底层同步机制帮助你理解 CPython 在无 GIL 模式下如何安全地维护类型系统的一致性。一、缺陷修复记录原文该条 NEWS 记录的全文如下Fix a deadlock in the free-threaded build between two threads assigning to a special method of the same class. While applying the type slot updates with the world stopped, only the type lock was prevented from being released; the type dict mutex could still be released and re-acquired, in the wrong order, if the thread blocked on the stop-the-world mutex.翻译并拆解其技术要点场景free-threaded 构建即--disable-gil编译出的 CPython下两个线程同时对同一个类的特殊方法special method如__repr__、__add__、__hash__进行赋值现象发生死锁根因在世界停止world stopped即 stop-the-world 暂停状态下应用类型插槽type slot更新时代码只阻止了type lock类型锁被释放而type dict mutex类型字典互斥锁在线程因阻塞在 stop-the-world 互斥锁上时仍可能被释放随后又以错误的顺序重新获取从而形成死锁。这条记录虽短却指向了无 GIL 构建中类型系统同步的一个关键设计问题。下面逐层深入源码验证。二、背景free-threaded 构建与类型锁体系CPython 的无 GIL 构建通过Py_GIL_DISABLED宏区分代码路径。Objects/typeobject.c 中大量#ifdef Py_GIL_DISABLED分支例如 Objects/typeobject.c#L56、Objects/typeobject.c#L1148即为该模式下的专用同步逻辑。2.1 两把互斥锁的分工在该构建下类型对象由两把锁共同保护见 Objects/typeobject.c#L56-L78TYPE_LOCK定义为_PyInterpreterState_GET()-types.mutex是解释器级位于_PyRuntime运行时结构内的全局类型锁。它保护tp_version_tag、_spec_cache、tp_mro、tp_bases、tp_base等关键字段的一致性type dict mutex即类型__dict__对象自身的ob_mutex堆上分配的对象锁保护对类型字典的写入。当同时需要两把锁时代码使用双互斥锁临界区#define BEGIN_TYPE_DICT_LOCK(d) \ Py_BEGIN_CRITICAL_SECTION2_MUTEX(TYPE_LOCK, _PyObject_CAST(d)-ob_mutex) #define END_TYPE_DICT_LOCK() Py_END_CRITICAL_SECTION2()关键细节在于两锁获取顺序注释明确指出Objects/typeobject.c#L138-L149type dict mutex 位于堆上而 TYPE_LOCK 位于_PyRuntime中因此双锁临界区按地址排序时通常先获取 dict mutex、后获取 TYPE_LOCK。这一顺序在后续死锁分析中至关重要。2.2 插槽更新为何要停世界给类的特殊方法赋值后CPython 需要重新计算对应的类型插槽如tp_repr、tp_hash。这一过程由update_one_slot()完成其注释Objects/typeobject.c#L11831-L11841说明If thequeued_updatespointer is provided, the actual updates to the slot pointers are queued, rather than being immediately performed. That argument is only used for the free-threaded build since those updates need to be done while the world is stopped.即无 GIL 构建下插槽指针的实际写入必须延迟到世界停止时统一执行否则其他线程可能在未加锁、无原子操作的情况下读到半更新的插槽。相关的队列应用函数apply_slot_updates()也以assert(types_world_is_stopped())强校验这一前提Objects/typeobject.c#L3853-L3869。2.3 世界停止机制stop-the-worldSTW暂停由解释器状态中的_stoptheworld_state支撑Include/internal/pycore_interp_structs.h#L410-L425struct _stoptheworld_state { PyMutex mutex; // Serializes stop-the-world attempts. bool requested; // Set when a pause is requested. bool world_stopped; // Set when the world is stopped. PyEvent stop_event; // Set when thread_countdown reaches zero. Py_ssize_t thread_countdown; // Number of threads that must pause. PyThreadState *requester; // Thread that requested the pause. };类型模块通过types_stop_world()/types_start_world()封装_PyEval_StopTheWorld()/_PyEval_StartTheWorld()Objects/typeobject.c#L116-L132在暂停期间独占执行插槽更新。三、根因阻塞时锁被释放再重取引发顺序反转要理解死锁必须先理解临界区与线程状态的关系。Include/internal/pycore_pystate.h#L20-L49 描述了无 GIL 构建下线程的三态模型[attached] - [detached] - [suspended]attached线程可执行 Python APIdetached线程不占用解释器资源可自行迁回 attachedsuspended仅用于实现 STW 暂停如循环垃圾回收。线程不能自行从 suspended 迁出只有执行暂停的线程能将其恢复为 detached。关键机制在于当持有临界区的线程发生阻塞如等待 STW 互斥锁时线程会被挂起detach其持有的临界区随之被挂起锁被释放。这正是缺陷的温床。结合 Objects/typeobject.c#L3871-L3903 中apply_type_slot_updates()的注释还原死锁场景线程 A 更新了类的__dict__持有了 TYPE_LOCK 与 type dict mutex准备应用插槽更新队列为避免其他线程在插槽更新完成前改写字典A 调用types_stop_world()请求 STW若 A 在等待 STW 互斥锁时发生阻塞旧代码只钉住prevent release了 TYPE_LOCKtype dict mutex 仍被释放恢复时_PyCriticalSection_Resume()需要在持有 TYPE_LOCK 的状态下重新获取 type dict mutex此时若线程 B 恰好持有 type dict mutex 并等待 TYPE_LOCK这正是BEGIN_TYPE_DICT_LOCK()的典型路径则 A 等 B 释放 dict mutex、B 等 A 释放 TYPE_LOCK——经典的 ABBA 死锁。注释原文一针见血地概括了这一点Objects/typeobject.c#L138-L145If only TYPE_LOCK was pinned then_PyCriticalSection_Resume()would have to re-acquire the other mutex while TYPE_LOCK is held. That deadlocks against a thread that holds that mutex and is waiting for TYPE_LOCK, which is exactly whatBEGIN_TYPE_DICT_LOCK()does.四、修复方案type_lock_prevent_release钉住全部互斥锁修复的核心思路是在等待 STW 期间不仅钉住 TYPE_LOCK而是钉住当前临界区持有的所有互斥锁使恢复时无需重新获取任何锁从根源上消除以错误顺序重取锁的可能性。4.1 数据结构与两个辅助函数钉住操作使用pinned_mutexes_t记录被借出的锁Objects/typeobject.c#L49-L54typedef struct { PyMutex *mutex1; PyMutex *mutex2; } pinned_mutexes_t;type_lock_prevent_release()Objects/typeobject.c#L150-L165从当前最顶层临界区中取出_cs_mutex必要时还有双锁临界区的_cs_mutex2将其置空并保存到pinned中。由于阻塞挂起时只会释放临界区中记录的锁置空后这些锁便不会因阻塞而被释放从而被钉住type_lock_allow_release()Objects/typeobject.c#L167-L183STW 结束、插槽更新完成后将锁归还给临界区恢复正常的挂起/恢复语义。4.2 修复后的调用序列修复后的apply_type_slot_updates()完整流程如下Objects/typeobject.c#L3871-L3903pinned_mutexes_t pinned; type_lock_prevent_release(pinned); // 1. 钉住 TYPE_LOCK 与 dict mutex types_stop_world(); // 2. 请求 STW阻塞期间锁不释放 apply_slot_updates(updates); // 3. 世界已停止安全写入插槽 types_start_world(); // 4. 恢复世界 type_lock_allow_release(pinned); // 5. 归还锁恢复常规语义同样_PyType_SetFlagsRecursive()Objects/typeobject.c#L6379-L6402在修改tp_flags时也采用了完全相同的先钉锁、再停世界模式保证在 STW 期间无人能重新分配 version tag。4.3 为什么钉住锁不会阻碍 STW一个自然的疑问是线程 A 持锁阻塞等待 STW会不会反过来导致其他线程无法进入暂停状态注释给出了答案Objects/typeobject.c#L146-L149Holding the mutexes while blocked does not prevent the world from being stopped: a thread waiting on either of them parks with_PY_LOCK_DETACHand so is detached while it waits.即其他线程在等待这两把锁时会以_PY_LOCK_DETACH方式挂起等待处于 detached 状态同样会被纳入 STW 暂停范围。因此钉住锁既保证了 A 自身的恢复路径安全也不会拖延 STW 的达成——这是该修复能够成立的两个关键前提。五、修复的普适模式与工程启示梳理本案例可以看到无 GIL 构建下类型系统同步的三个核心原则插槽/标志等非原子字段的更新必须发生在世界停止时这是数据一致性的底线由ASSERT_WORLD_STOPPED_OR_NEW_TYPE等断言Objects/typeobject.c#L104-L106在调试构建中强制校验临界区恢复时不得以颠倒的顺序重取锁通过type_lock_prevent_release全量钉住临界区持有的锁保证释放→重取路径根本不会发生持有锁等待 STW 是安全的因为等待方会 detached从而被 STW 机制覆盖不会形成持锁者阻止停世界的间接死锁。这套模式已在apply_type_slot_updates()与_PyType_SetFlagsRecursive()两处落地属于 free-threaded 构建中类型修改路径的标准同步范式。六、验证与复现思路若需在本地验证该修复可先以无 GIL 模式构建当前仓库配置阶段指定--disable-gil构建产物即 free-threaded 解释器相关代码路径由Py_GIL_DISABLED宏启用随后编写多线程脚本两个线程反复对同一个类的同一特殊方法如__repr__并发赋值循环多轮以放大竞争窗口。修复前在该竞争下可能挂起修复后应稳定运行。可配合调试构建Py_DEBUG运行此时 Objects/typeobject.c 中ASSERT_TYPE_LOCK_HELD、ASSERT_WORLD_STOPPED_OR_NEW_TYPE等断言会在锁序或停世界条件不满足时第一时间失败帮助定位问题。七、小结本缺陷修复记录虽仅有数行却浓缩了 CPython free-threaded 构建中一个典型的锁序死锁案例世界停止等待期间只钉住类型锁而放行字典锁导致临界区恢复时以错误顺序重取锁与持有字典锁、等待类型锁的线程形成 ABBA 死锁。修复通过type_lock_prevent_release将临界区持有的全部互斥锁一并钉住配合等待者 detached 不影响 STW的机制既消除了错误重取路径又不引入新的暂停阻碍。理解这一案例对深入研读无 GIL 构建的类型系统、垃圾回收STW 的另一主要用户乃至任何停世界 多锁同步设计都有直接参考价值。【免费下载链接】cpythonThe Python programming language项目地址: https://gitcode.com/GitHub_Trending/cp/cpython创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表