Python修改内置LockType.acquire提前引用原方法致装饰器失效问题
问题背景
需要在Python中对锁的acquire操作进行注册记录,受场景限制无法继承_thread.LockType并重写其acquire方法,因此尝试为_thread.LockType.acquire包装自定义装饰器。
参考内置类型扩展的实现方案,通过ctypes绕过_thread.LockType.__dict__的mappingproxy限制,实现代码如下:
import ctypes from ctypes import pythonapi as api from ctypes import py_object from _thread import LockType class PyObject(ctypes.Structure): """ 参考Python C API定义,所有对象类型都是该类型的扩展 至少包含两个成员: - 'ob_refcnt' Py_ssize_t类型,为对象引用计数器 - 'ob_type' PyTypeObject类型,指向Python对象的类型对象 """ pass """ Py_ssize_t为有符号整型,C层面其长度与size_t一致 """ Py_ssize_t = ctypes.sizeof(ctypes.c_voidp) Py_ssize_t = ctypes.c_int64 if ctypes.sizeof(ctypes.c_int64) == Py_ssize_t else ctypes.c_int32 PyObject._fields_ = [ ('ob_refcnt', Py_ssize_t), ('ob_type', ctypes.POINTER(PyObject)) ] class SlotsPointer(PyObject): _fields_ = [('dict', ctypes.POINTER(PyObject))] mapping_proxy = getattr(LockType, '__dict__') proxy_pointer = Dict.from_address(id(mapping_proxy)) namespace = {} api.PyDict_SetItem(py_object(namespace), py_object(LockType.__name__), proxy_pointer.dict) lck_dict = namespace[LockType.__name__] lck_dict['acquire'] = my_acquire_decorator # 签名与acquire方法匹配的自定义装饰器
问题现象
上述代码在从未显式引用_thread.LockType.acquire时可正常运行,但存在以下异常表现:
- 若为了在装饰器内部调用原逻辑,执行
original_acquire = LockType.acquire保存原acquire方法,自定义装饰器会完全失效,仿佛lck_dict['acquire'] = my_acquire_decorator的赋值操作从未生效 - 哪怕仅调用
sys.getrefcount(LockType.acquire)获取该方法的引用计数,也会导致修改失效,无法确定问题是否由持有原方法引用导致
曾尝试通过如下流程排查问题:
- 关闭垃圾回收自动收集机制
- 保存原方法引用
- 手动清空原方法的引用计数
- 注册自定义装饰器
- 手动增加原方法的引用计数
- 重新开启垃圾回收
该操作直接触发段错误(Segfault),未能解决问题。
原因说明
该现象由CPython解释器对内置C类型的方法查找与缓存机制导致:
- 方法存储逻辑差异:
_thread.LockType是C实现的内置类型,它的acquire方法本质是注册在类型对象C层槽位的原生函数,并非存储在类__dict__中的普通Python函数。从未访问过LockType.acquire时,类__dict__中不存在acquire键,属性查找会遍历dict内容,通过ctypes绕过限制塞入的自定义装饰器会被命中,因此可以正常生效。 - 访问触发的缓存优先级问题:一旦以任何形式(赋值引用、获取引用计数等)访问过
LockType.acquire,CPython会自动将C层原生函数包装为Python可调用的方法对象,同时将该对象缓存到类型内部的槽位缓存中。后续所有对LockType.acquire的属性查找,会优先返回槽位缓存的原生方法对象,完全跳过对类__dict__的查找流程,因此修改底层dict的操作不会再生效。 - 段错误触发原因:手动修改原生方法对象的引用计数属于不安全操作。C实现的内置方法对象的生命周期、内存布局完全由CPython解释器内部管理,强行将引用计数清零会让解释器误判对象可回收,直接释放对应内存,后续访问该内存地址就会触发野指针访问,直接导致段错误。
内容的提问来源于stack exchange,提问作者vultkayn
相关产品推荐
相关产品推荐

