拓冰建站拓冰建站
首页 / 资讯中心 / 正文

CPython C 扩展同步原语实战指南:PyMutex、Critical Section 与 Legacy 线程锁 API

CPython C 扩展同步原语实战指南PyMutex、Critical Section 与 Legacy 线程锁 API【免费下载链接】cpythonThe Python programming language项目地址: https://gitcode.com/GitHub_Trending/cp/cpythonCPython 的 C API 为扩展模块提供了三层同步设施3.13 引入的轻量互斥锁PyMutex、面向 free-threaded 构建的死锁规避层 Critical Section API以及历史上被大量 C 扩展依赖的PyThread_*系列锁 API。本文以官方文档 synchronization.rst 为主体结合 Include/cpython/pylock.h、Python/lock.c、Python/thread.c 和 Python/critical_section.c 的源码实现逐层拆解每个 API 的语义、约束与底层机制帮助你为 C 扩展正确选型并使用这些同步原语。三层同步 API 总览文档将同步原语组织为三个部分各自的定位和适用版本如下API引入版本定位典型使用场景PyMutex及PyMutex_Lock/PyMutex_Unlock/PyMutex_IsLocked3.13IsLocked为 3.14基础互斥锁提供排他访问保护 C 结构体内部状态流、解析器等Critical Section 宏与底层函数3.13Mutex 变体为 3.14free-threaded 下的死锁规避层GIL 构建中为空操作C-API 自定义类型的 per-object 保护LegacyPyThread_*锁 API3.13 起标记为过时3.15 起成为PyMutex的简单包装兼容旧扩展维护既有代码需要特别说明的是 Critical Section 的生效前提它专为 free-threaded无 GIL构建设计在带 GIL 的默认构建中所有相关函数、宏都是 no-op。文档与 Include/critical_section.h 头文件注释均明确标注了这一点。PyMutex只占一个字节的互斥锁类型定义与使用约束PyMutex是一个互斥锁类型核心使用约定在文档中非常明确零初始化必须以 0 初始化以表示未加锁状态惯用写法为PyMutex mutex {0};禁止拷贝与移动PyMutex的内容和地址都有语义必须固定存放在内存中可写的、地址不变的位置尺寸不稳定目前PyMutex只占 1 个字节但文档明确警告这个尺寸不应被视为稳定未来版本可能在无弃用期的情况下改变。这一点在源码中得到了印证。Include/cpython/pylock.h 中的定义是typedef struct PyMutex { uint8_t _bits; // (private) } PyMutex;头文件注释同时说明了位编码仅供理解实现和调试不属于公开 API0b00未加锁、0b01已加锁、0b10未加锁但有线程在等待parked、0b11已加锁且有等待线程。也就是说单个字节的低两位编码了完整的锁状态这是它能压缩到 1 字节的原因。加锁、解锁与状态查询三个函数构成完整的操作集PyMutex_Lock(PyMutex *m)加锁。若其他线程已持有该锁调用线程会阻塞直到锁被释放。阻塞期间若当前线程持有 attached thread state运行时会临时将其 detach。这一阻塞时 detach的行为在实现中对应 Python/lock.c 里PyMutex_Lock调用_PyMutex_LockTimed(m, -1, _PY_LOCK_DETACH)时传入的_PY_LOCK_DETACH标志。PyMutex_Unlock(PyMutex *m)解锁。锁必须处于已加锁状态否则函数触发 fatal error。这并非静默失败从源码 Python/lock.c 可见_PyMutex_TryUnlock返回 -1 时直接调用Py_FatalError(unlocking mutex that is not locked)终止进程——误用解锁是 CPython 视为不可恢复的编程错误。PyMutex_IsLocked(PyMutex *m)3.14 新增当前已加锁返回非零否则返回 0。文档特别提示该函数只应用于断言和调试不应用于并发控制决策因为检查之后锁状态随时可能改变。加锁的内部路径快速 CAS 与慢速 parking lotPyMutex_Lock在头文件中被展开为内联函数走两级路径见 Include/cpython/pylock.hstatic inline void _PyMutex_Lock(PyMutex *m) { uint8_t expected _Py_UNLOCKED; if (!_Py_atomic_compare_exchange_uint8(m-_bits, expected, _Py_LOCKED)) { PyMutex_Lock(m); // 慢速路径 } }无竞争时一次原子 compare-exchange 即完成加锁竞争发生时才进入慢速路径。慢速路径的核心在 Python/lock.c 的_PyMutex_LockTimed其中有几个值得注意的设计有限自旋仅 free-threaded 构建Py_GIL_DISABLED启用自旋最多MAX_SPIN_COUNT 40次sched_yield()并借助线程 ID 低位错开各线程的 CAS 重试时机降低竞争带 GIL 的构建自旋次数为 0。1 毫秒公平性移交TIME_TO_BE_FAIR_NS定义为 1 ms——如果某个线程等待锁超过 1 毫秒解锁线程会把锁的所有权直接移交给它handed_off标志避免等待线程饥饿。parking lot 等待自旋失败后线程通过_PyParkingLot_Park挂起唤醒逻辑见 Python/lock.c 的mutex_unpark。仓库内部模块本身就是这套 API 的现成用法示例例如 Modules/_bz2module.c 在流读取前后成对调用PyMutex_Lock(self-mutex); /* ... 操作共享状态 ... */ PyMutex_Unlock(self-mutex);类似的成对模式也出现在 Modules/_lzmamodule.c、Modules/_ssl.c 等处可作为编写自有扩展时的参考范式。Python Critical Section APIfree-threaded 下的死锁规避层设计动机为什么锁不够无 GIL 构建中每个 Python 对象都带有 per-object 锁。如果扩展代码持有对象 A 的锁又在临界区内调用任意 C API 函数而这些函数可能反过来加锁就可能形成死锁——典型场景是Py_DECREF触发对象析构而析构函数里的代码又试图加锁。Critical Section API 就是为了解决这个问题它不是传统意义上的锁而是一个会隐式挂起suspend的死锁规避层。开始临界区时获取对象的 per-object 锁但临界区内部的 C API 调用可以挂起该临界区——暂时释放 per-object 锁让其他线程得以访问同一对象——事后再恢复。因此它不提供PyMutex那样的排他访问而是提供逻辑上的临界区。文档给出的官方示例正是这个语义的体现static PyObject * set_field(MyObject *self, PyObject *value) { Py_BEGIN_CRITICAL_SECTION(self); Py_SETREF(self-field, Py_XNewRef(value)); Py_END_CRITICAL_SECTION(); Py_RETURN_NONE; }Py_SETREF内部会调用Py_DECREF可能触发任意析构代码。如果该代码阻塞并调用PyEval_SaveThread运行时就会挂起当前临界区从而避免由重入和锁顺序引起的死锁。适用边界自定义类型而非内建类型文档明确了适用范围Critical Section 面向用 C API 实现的自定义类型。一般不应在list、dict等内建类型上使用因为它们的公开 C API 内部已经使用了临界区唯一的例外是PyDict_Next——它要求调用者从外部持有临界区。另一个硬约束需要同时锁定两个对象的操作必须使用Py_BEGIN_CRITICAL_SECTION2。不能用嵌套临界区同时锁两个对象因为内层临界区可能挂起外层且该 API 不提供一次锁定超过两个对象的途径。五个宏及其展开形式宏是首选接口。文档对每个宏给出了在 free-threaded 构建以及 Stable ABI 构建下的展开形态并说明默认构建下均退化为普通花括号Py_BEGIN_CRITICAL_SECTION(op)3.13获取op的 per-object 锁并开始临界区。展开为声明一个PyCriticalSection _py_cs并调用PyCriticalSection_Begin(_py_cs, (PyObject*)(op))。Py_BEGIN_CRITICAL_SECTION_MUTEX(m)3.14锁定互斥锁m并开始临界区展开后调用PyCriticalSection_BeginMutex(_py_cs, m)。与对象变体不同该宏不对参数做转换m必须是PyMutex指针——适用于不继承或包装PyObject、但仍需调用 C API 的 C 类型。Py_END_CRITICAL_SECTION()3.13结束临界区并释放锁展开为PyCriticalSection_End(_py_cs); }。Py_BEGIN_CRITICAL_SECTION2(a, b)3.13同时获取两个对象的 per-object 锁。锁按**一致顺序地址低者优先**获取从机制上避免锁顺序死锁。Py_BEGIN_CRITICAL_SECTION2_MUTEX(m1, m2)3.14双互斥锁版本参数同样不做转换必须是两个PyMutex指针。Py_END_CRITICAL_SECTION2()3.13对应结束释放两把锁。宏的公开声明位于 Include/critical_section.h 与 Include/cpython/critical_section.h其中 Mutex 变体宏的展开PyCriticalSection_BeginMutex等仅对 free-threaded 与 Stable ABI 构建生效。头文件注释还补充了一条文档未展开的细节只有最外层的临界区保证处于活动状态且临界区在递归获取同一把锁时表现为类可重入锁但效率不及专门设计的可重入锁。底层函数与私有结构体对 C 宏不可用的场景如某些受限构建API 同时导出了底层函数PyCriticalSection_Begin/PyCriticalSection_End3.13对象版本签名void (PyCriticalSection *c, PyObject *op)等。文档强调只能按宏展开的形态使用在非 free-threaded 构建中这些函数什么都不做。PyCriticalSection2_Begin/PyCriticalSection2_End3.13双对象版本。PyCriticalSection_BeginMutex/PyCriticalSection2_BeginMutex3.14互斥锁版本。结构体PyCriticalSection/PyCriticalSection2内容私有语义可能在未来版本改变。从 Include/critical_section.h 可以看到PyCriticalSection实际包含一个指向外层临界区的 tagged 指针_cs_prev和一个PyMutex *_cs_mutex——即线程上的临界区构成一条链PyCriticalSection2在此之上追加第二个_cs_mutex2。源码中这些行为的实现位于 Python/critical_section.c 和 Include/internal/pycore_critical_section.h。几个关键机制值得了解线程状态上的临界区链_PyCriticalSection_BeginSlow把新临界区压入tstate-critical_section_cs_prev保存链上外层节点递归优化若最外层临界区持有的正是同一把锁则直接跳过加锁_cs_mutex置空见 Python/critical_section.c世界停止检查若stoptheworld.world_stopped为真单线程阶段无需加锁否则可能导致死锁Python/critical_section.c挂起与恢复线程 detach 时_PyCriticalSection_SuspendAll会沿链解锁所有活动临界区并打上INACTIVE标记attach 后由_PyCriticalSection_Resume按序重新加锁——这正是文档所述阻塞期间临时 detach / 挂起临界区的实现落点Python/critical_section.c。Legacy 锁 APIPyThread_*系列的现状PyThread_allocate_lock等 API 是最老的一代线程锁接口。文档给出的版本时间线是自 3.13 引入PyMutex起这批 API 已被标记为过时obsolete自 3.15 起它们已成为PyMutex的简单包装。源码完全印证了这一点Python/thread.c 中PyThread_allocate_lock如今就是分配一块零初始化的PyMutexPyMem_RawMalloc(sizeof(PyMutex))后写入(PyMutex){0}PyThread_free_lock则只是PyMem_RawFree。类型与状态常量PyThread_type_lock互斥锁的指针PyLockStatus带超时加锁的结果枚举值包括PY_LOCK_FAILURE获取失败、PY_LOCK_ACQUIRED获取成功、PY_LOCK_INTR被信号中断以及用.. c:namespace:: NULL表示的NULL。各函数的语义与实现要点PyThread_allocate_lock(void)分配新锁。成功返回锁失败返回0且不设异常。调用者无需持有 attached thread state。3.15 起恒定使用PyMutex更早版本则使用操作系统提供的锁。PyThread_free_lock(PyThread_type_lock lock)销毁锁。调用时锁不得被任何线程持有。同样不要求 attached thread state。PyThread_acquire_lock_timed(PyThread_type_lock lock, long long microseconds, int intr_flag)带超时加锁。等待microseconds微秒超时返回PY_LOCK_FAILUREmicroseconds为-1表示无限等待。intr_flag为 1 时加锁可被信号中断此时返回PY_LOCK_INTR调用者一般应调用Py_MakePendingCalls把异常传播回 Python 代码。成功返回PY_LOCK_ACQUIRED。从源码看Python/thread.c微秒值会先经_PyTime_FromMicrosecondsClamp钳制到合法纳秒范围防止 bpo-41710 的超时溢出问题intr_flag则映射为_PY_FAIL_IF_INTERRUPTED标志传入_PyMutex_LockTimed。PyThread_acquire_lock(PyThread_type_lock lock, int waitflag)waitflag为 1 且锁被占用时阻塞等待最终必定返回 1waitflag为 0 且锁被占用时立即返回 0锁空闲则获取并返回 1。与 timed 版本不同该路径的加锁不能被信号中断——实现上就是PyThread_acquire_lock_timed(lock, waitflag ? -1 : 0, /*intr_flag*/0)Python/thread.c。PyThread_release_lock(PyThread_type_lock lock)释放锁若锁未被持有则触发 fatal error底层同样是PyMutex_Unlock→Py_FatalError。上述五个函数全部声明调用者无需持有 attached thread state这使得它们可以在 Python 线程机制初始化前后、甚至在嵌入场景中安全使用。选型建议新代码写什么结合文档与源码可以给出一条清晰的选型路径新扩展模块保护自身 C 状态直接用PyMutexPyMutex mutex {0};初始化成对PyMutex_Lock/PyMutex_Unlock这是 3.13 起的推荐方式无需 GIL 参与自定义类型的 per-object 保护用Py_BEGIN_CRITICAL_SECTION/Py_BEGIN_CRITICAL_SECTION2宏而不是裸锁——在 free-threaded 构建中它们自带挂起/恢复的死锁规避在 GIL 构建中零成本无PyObject上下文时3.14用Py_BEGIN_CRITICAL_SECTION_MUTEX变体维护既有PyThread_*代码可以原样保留因为 3.15 起它已经与PyMutex同源但不应在新代码中继续引入这组 API避免两类误用不要用嵌套临界区锁两个以上对象必须用 SECTION2 宏且最多两个不要用PyMutex_IsLocked的结果做并发控制决策仅限断言与调试。以上所有 API 的完整签名、版本标注与语义说明以仓库内的官方文档 Doc/c-api/synchronization.rst 为准实现细节可进一步对照 Include/cpython/pylock.h、Python/lock.c、Python/thread.c 与 Python/critical_section.c 深入阅读。【免费下载链接】cpythonThe Python programming language项目地址: https://gitcode.com/GitHub_Trending/cp/cpython创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门