问题
指向cdef类的void指针指向相同的内存地址,而不强制python引用计数器 .
说明
我有一个简单的类,我想通过将它转换为void指针存储在cpp向量中 . 但是,在打印指针指向的内存地址后,它会在第二次迭代后重复, unless 我通过将新对象添加到列表来强制增加引用计数器 . 有人为什么内存循环没有参考计数器执行?
# distutils: language = c++
# distutils: extra_compile_args = -std=c++11
from libcpp.vector cimport vector
from libc.stdio cimport printf
cdef class Temp:
cdef int a
def __init__(self, a):
self.a = a
def f():
cdef vector[void *] vec
cdef int i, n = 3
cdef Temp tmp
cdef list ids = []
# cdef list classes = [] # force reference counter?
for i in range(n):
tmp = Temp(1)
# classes.append(tmp)
vec.push_back(<void *> tmp)
printf('%p ', <void *> tmp)
ids.append(id(tmp))
print(ids)
f()
哪个输出:
[140137023037824, 140137023037848, 140137023037824]
但是,如果我通过将引用计数器添加到类列表来强制它:
[140663518040448, 140663518040472, 140663518040496]
2 回答
这个答案变得很长,因此可以快速浏览内容:
观察到的行为的说明
天真的方法来避免这个问题
更系统,更典型的解决方案
解释了"nogil" -mode中多线程代码的问题
为nogil模式扩展c-ticalpical解决方案
Explanation of the observed behavior
与Cython的交易:只要您的变量属于
object
类型或从它继承(在您的情况下cdef Temp
),cython将为您管理引用计数 . 只要将其转换为PyObject *
或任何其他指针 - 引用计数就是您的责任 .显然,对创建的对象的唯一引用是变量
tmp
,只要将其重新绑定到新创建的Temp
object,旧对象的引用计数器就会变为0
并且它被销毁 - 向量中的指针变得悬空 . 但是,可以重用相同的内存(很可能),因此您总是看到相同的重用地址 .Naive solution
你怎么能做引用计数?例如(我使用
PyObject *
而不是void *
):现在所有对象都保持活动状态"die"只有在显式调用
Py_XDECREF
之后 .C++-typical solution
以上不是一个非常典型的做法,我宁愿介绍一个自动管理引用计数的包装器(与
std::shared_ptr
不同):值得注意的事情:
PyObjectHolder
只要拥有一个PyObject
-pointer就会增加ref-counter,并在释放指针后立即减少它 .三条法则意味着我们还必须注意复制构造函数和赋值运算符
我省略了针对c 11的移动内容,但你也需要处理它 .
Problems with nogil-mode
然而,有一个非常重要的事情: You shouldn't release GIL 具有上述实现(即将其导入
PyObjectHolder(PyObject *o) nogil
但是当C复制向量时也存在问题) - 因为否则Py_XINCREF
和Py_XDECREF
可能无法正常工作 .为了说明这一点,我们来看看下面的代码,它释放gil并且并行执行一些愚蠢的计算(整个魔术单元在答案的最后是列表中):
现在:
我们很幸运,程序没有崩溃(但可能!) . 但是由于竞争条件,我们最终导致内存泄漏 -
a[0]
的引用计数为1177
但是只有1000个引用(sys.getrefcount
内部有2个)引用,因此该对象永远不会被销毁 .Making PyObjectHolder thread-safe
那么该怎么办?最简单的解决方案是使用互斥锁来保护对ref-counter的访问(即每次调用
Py_XINCREF
或Py_XDECREF
时) . 这种方法的缺点是它可能会显着降低单核代码的速度(例如,参见this old article关于通过类似互斥的方法替换GIL的旧尝试) .这是一个原型:
现在,运行从上面剪切的代码会产生预期/正确的行为:
列出完整的线程不安全版本:
您的对象最终位于同一地址的事实是巧合 . 你的问题是你的python对象在它们的最后一个python引用消失时被销毁 . 如果你想让python对象保持活着,你需要在某处保存对它们的引用 .
在您的情况下,由于
tmp
是您在循环中创建的Temp
对象的唯一引用,因此每次重新分配tmp
时,它先前引用的对象都将被销毁 . 这会在内存中留下一个空白空间,它可以很方便地保存在循环的下一次迭代中创建的Temp
对象的正确大小,从而导致您在指针中看到的交替模式 .