指向相同地址的无效指针

时间:2018-12-09 19:05:39

标签: c++ python-3.x memory-management cython void-pointers

问题

指向cdef类的无效指针指向相同的内存地址,而没有强制使用python引用计数器。

说明

我有一个简单的类,希望通过将其转换为void指针存储在cpp向量中。但是,在打印了指针所指向的内存地址之后,它会在第二次迭代之后重复执行,除非,我通过将新对象添加到列表中来强制增加引用计数器。有人可以在没有引用计数器强制的情况下为何回显内存吗?

# 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 个答案:

答案 0 :(得分:4)

这个答案变得很长,因此可以快速浏览内容:

  1. 观察到的行为的解释
  2. 避免问题的天真方法
  3. 更系统,更典型的c ++解决方案
  4. “ nogil”模式下的多线程代码说明问题
  5. 扩展针对nogil模式的c ++典型解决方案

观察到的行为的解释

与Cython的交易:只要变量的类型为object或从中继承(在您的情况下为cdef Temp),cython就会为您管理引用计数。一旦将其转换为PyObject *或任何其他指针,引用计数就是您的责任。

很明显,对创建对象的唯一引用是变量tmp,将其重新绑定到新创建的Temp对象后,旧对象的引用计数器就变成{{ 1}}并且它被破坏了-向量中的指针变得悬空了。但是,相同的内存可以重复使用(很有可能),因此您总是看到相同的重复使用地址。

天真解决方案

您如何进行引用计数?例如(我使用的是0而不是PyObject *):

void *

现在,所有对象都保持活动状态,并且仅在显式调用... from cpython cimport PyObject,Py_XINCREF, Py_XDECREF ... def f(): cdef vector[PyObject *] vec cdef int i, n = 3 cdef Temp tmp cdef PyObject *tmp_ptr cdef list ids = [] for i in range(n): tmp = Temp(1) tmp_ptr = <PyObject *> tmp Py_XINCREF(tmp_ptr) # ensure it is not destroyed vec.push_back(tmp_ptr) printf('%p ', tmp_ptr) ids.append(id(tmp)) #free memory: for i in range(n): Py_XDECREF(vec.at(i)) print(ids) 之后“死亡”。

C ++典型解决方案

以上内容不是一种非常典型的c ++处理方式,我宁愿介绍一种自动管理引用计数的包装器(与Py_XDECREF不同):

std::shared_ptr

值得注意的事情:

  1. ... cdef extern from *: """ #include <Python.h> class PyObjectHolder{ public: PyObject *ptr; PyObjectHolder():ptr(nullptr){} PyObjectHolder(PyObject *o):ptr(o){ Py_XINCREF(ptr); } //rule of 3 ~PyObjectHolder(){ Py_XDECREF(ptr); } PyObjectHolder(const PyObjectHolder &h): PyObjectHolder(h.ptr){} PyObjectHolder& operator=(const PyObjectHolder &other){ Py_XDECREF(ptr); ptr=other.ptr; Py_XINCREF(ptr); return *this; } }; """ cdef cppclass PyObjectHolder: PyObjectHolder(PyObject *o) ... def f(): cdef vector[PyObjectHolder] vec cdef int i, n = 3 cdef Temp tmp cdef PyObject *tmp_ptr cdef list ids = [] for i in range(n): tmp = Temp(1) vec.push_back(PyObjectHolder(<PyObject *> tmp)) # vector::emplace_back is missing in Cython-wrappers printf('%p ', <PyObject *> tmp) ids.append(id(tmp)) print(ids) # PyObjectHolder automatically decreases ref-counter as soon # vec is out of scope, no need to take additional care 一旦拥有PyObjectHolder指针就增加引用计数器,并在释放指针后立即减小引用计数器。
  2. 三个规则意味着我们还必须注意复制构造函数和赋值运算符
  3. 我已经省略了c ++ 11的move-stuff,但是您也需要注意这一点。

使用nogil模式的问题

但是有一件非常重要的事情:您不应该使用上述实现发布GIL (即,将其导入为PyObject,但是C ++复制矢量和类似的矢量时也会出现问题)-因为否则PyObjectHolder(PyObject *o) nogilPy_XINCREF可能无法正常工作。

为说明这一点,让我们看下面的代码,该代码释放gil并并行执行一些愚蠢的计算(整个魔术单元格位于答案的结尾处):

Py_XDECREF

现在:

%%cython --cplus -c=/openmp 
...
# importing as nogil - A BAD THING
cdef cppclass PyObjectHolder:
    PyObjectHolder(PyObject *o) nogil

# some functionality using a lot of incref/decref  
cdef int create_vectors(PyObject *o) nogil:
    cdef vector[PyObjectHolder] vec
    cdef int i
    for i in range(100):
        vec.push_back(PyObjectHolder(o))
    return vec.size()

# using PyObjectHolder without gil - A BAD THING
def run(object o):
    cdef PyObject *ptr=<PyObject*>o;
    cdef int i
    for i in prange(10, nogil=True):
        create_vectors(ptr)

我们很幸运,该程序没有崩溃(但是可以!)。但是由于竞争条件,我们最终发生了内存泄漏-import sys a=[1000]*1000 print("Starts with", sys.getrefcount(a[0])) # prints: Starts with 1002 run(a[0]) print("Ends with", sys.getrefcount(a[0])) #prints: Ends with 1177 的引用计数为a[0],但是只有1000个引用(1177内的+2)有效,因此此对象永远不会被摧毁。

使sys.getrefcount具有线程安全性

那该怎么办?最简单的解决方案是使用互斥锁来保护对引用计数器的访问(即每次调用PyObjectHolderPy_XINCREF时)。这种方法的缺点是,它可能会大大降低单个核心代码的速度(例如,参见this old article,这是一种通过互斥体相似的方法替换GIL的较早尝试)。

这是一个原型:

Py_XDECREF

现在,运行从上方截断的代码会产生预期的/正确的行为:

%%cython --cplus -c=/openmp 
...
cdef extern from *:
    """
    #include <Python.h>
    #include <mutex>

    std::mutex ref_mutex;

    class PyObjectHolder{
    public:
        PyObject *ptr;
        PyObjectHolder():ptr(nullptr){}
        PyObjectHolder(PyObject *o):ptr(o){
            std::lock_guard<std::mutex> guard(ref_mutex);
            Py_XINCREF(ptr);
        }
        //rule of 3
        ~PyObjectHolder(){
            std::lock_guard<std::mutex> guard(ref_mutex);
            Py_XDECREF(ptr);
        }
        PyObjectHolder(const PyObjectHolder &h):
            PyObjectHolder(h.ptr){}
        PyObjectHolder& operator=(const PyObjectHolder &other){
            {
                std::lock_guard<std::mutex> guard(ref_mutex);
                Py_XDECREF(ptr);
                ptr=other.ptr;
                Py_XINCREF(ptr);
            }
            return *this;
        }
    };
    """
    cdef cppclass PyObjectHolder:
        PyObjectHolder(PyObject *o) nogil
    ...

但是,正如@DavidW指出的那样,使用import sys a=[1000]*1000 print("Starts with", sys.getrefcount(a[0])) # prints: Starts with 1002 run(a[0]) print("Ends with", sys.getrefcount(a[0])) #prints: Ends with 1002 仅适用于openmp线程,不适用于Python解释器创建的线程。

以下是互斥体解决方案将失败的示例。

首先,将nogil函数包装为std::mutex函数:

def

现在使用%%cython --cplus -c=/openmp ... def single_create_vectors(object o): cdef PyObject *ptr=<PyObject *>o with nogil: create_vectors(ptr) 模块创建

threading

使用import sys a=[1000]*10000 # some safety, so chances are high python will not crash print(sys.getrefcount(a[0])) #output: 10002 from threading import Thread threads = [] for i in range(100): t = Thread(target=single_create_vectors, args=(a[0],)) threads.append(t) t.start() for t in threads: t.join() print(sys.getrefcount(a[0])) #output: 10015 but should be 10002! 的另一种方法是使用Python机器,即PyGILState_STATE,这将导致代码类似于

std::mutex

这也适用于上面的... PyObjectHolderPy(PyObject *o):ptr(o){ PyGILState_STATE gstate; gstate = PyGILState_Ensure(); Py_XINCREF(ptr); PyGILState_Release(gstate); } ... -示例。但是,threading的开销太高-对于上面的示例,它的速度比互斥锁解决方案慢100倍左右。使用Python机械的另一种轻量级解决方案也将带来更多麻烦。


列出完整的线程不安全版本:

PyGILState_Ensure

答案 1 :(得分:2)

您的对象最终到达同一地址这一事实是巧合。您的问题是,当对它们的最后一个python引用消失时,您的python对象将被破坏。如果要使python对象保持活动状态,则需要在某个地方保留对它们的引用。

在您的情况下,由于tmp是对您在循环中创建的Temp对象的唯一引用,因此,每次重新分配tmp时,它先前引用的对象都会被获取。毁了。这会在内存中留下空白空间,该空白大小恰好恰好适合容纳在循环的下一次迭代中创建的Temp对象,从而导致您在指针中看到交替的模式。