代码之家  ›  专栏  ›  技术社区  ›  cvanelteren

指向同一地址的空指针

  •  1
  • cvanelteren  · 技术社区  · 7 年前

    问题

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

    描述

    我有一个简单的类,我想通过将它强制转换为一个空指针来存储在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 回复  |  直到 7 年前
        1
  •  4
  •   ead    7 年前

    这个答案变得相当长,因此对内容进行了快速概述:

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

    观察行为的解释

    处理赛通:只要你的变量是类型 object 或者从中继承 cdef Temp )Cython为您管理参考计数。只要你把它扔到 PyObject * 或任何其他指针-引用计数是您的责任。

    显然,对所创建对象的唯一引用是变量 tmp ,一旦将其重新绑定到新创建的 Temp -对象,旧对象的引用计数器变为 0 它被破坏了,向量中的指针开始摆动。但是,相同的内存可以重用(很可能是这样),因此您总是可以看到相同的重用地址。

    幼稚的解决方案

    你如何计算参考数据?例如(我使用 PyObjult* 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)
    

    现在所有的物体都是活的,只有在 Py_XDECREF 显式调用。

    C++典型解

    以上并不是一个非常典型的C++操作方法,我宁愿引入一个自动管理引用计数的包装器(不一样)。 std::shared_ptr )以下内容:

    ...
    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
    

    值得注意的是:

    1. PyObjectHolder 一旦拥有一个 PyObject -指针,并在释放指针后立即将其减小。
    2. 三条规则意味着我们还必须注意复制构造函数和赋值运算符
    3. 我已经省略了C++ 11的移动内容,但是你也需要照顾它。

    Nogil模式的问题

    然而,有一件非常重要的事情: 你不应该释放吉尔 使用上述实现(即导入为 PyObjectHolder(PyObject *o) nogil 但是,当C++复制向量和类似物时也存在问题-因为否则 Py_XINCREF PyxxRefff 可能无法正常工作。

    为了说明这一点,让我们来看看下面的代码,它发布了gil,并并行执行了一些愚蠢的计算(答案末尾的列表中列出了整个魔力单元):

    %%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] 引用计数为 1177 但是只有1000个参考(+2个在 sys.getrefcount )引用是活动的,因此永远不会销毁此对象。

    制作 PyObjutthHythOver 线程安全

    那么该怎么办呢?最简单的解决方案是使用互斥来保护对引用计数器的访问(即每次 PyxxPrIFF PyxxRefff 被称为。这种方法的缺点是它可能会大大降低单核代码的速度(例如,请参见 this old article 关于一个更老的尝试用mutex类似的方法来代替gil)。

    这是一个原型:

    %%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
        ...
    

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

    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
    

    然而,正如@davidw指出的,使用 std::mutex 仅适用于OpenMP线程,但不适用于由Python解释器创建的线程。

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

    首先,将nogil函数包装为 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!
    

    替代使用 STD::互斥 将使用python机器,即 PyGILState_STATE ,这将导致类似于

    ...
    PyObjectHolderPy(PyObject *o):ptr(o){
        PyGILState_STATE gstate;
        gstate = PyGILState_Ensure();
        Py_XINCREF(ptr);
        PyGILState_Release(gstate);
    }
    ...
    

    这也适用于 螺纹加工 -上面的例子。然而, PyGILState_Ensure 开销太大——例如上面的例子,它比互斥解决方案慢100倍。使用python机器的一个更轻的解决方案也意味着更麻烦。


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

    %%cython --cplus -c=/openmp 
    
    from libcpp.vector cimport vector
    from libc.stdio cimport printf
    from cpython cimport PyObject  
    from cython.parallel import prange
    
    import sys
    
    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) nogil
    
    
    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()
    
    def run(object o):
        cdef PyObject *ptr=<PyObject*>o;
        cdef int i
        for i in prange(10, nogil=True):
            create_vectors(ptr)
    
        2
  •  2
  •   Miles Budnek    7 年前

    你的物品最终在同一个地址是巧合。您的问题是,当最后一个对python对象的引用消失时,您的python对象就会被销毁。如果希望保持Python对象的活动状态,则需要在某个地方保存对它们的引用。

    在你的情况下,因为 tmp 是唯一引用 Temp 每次重新分配时,在循环中创建的对象 川芎嗪 ,它以前引用的对象将被销毁。它在内存中留下了空白空间,方便地精确地容纳 打临时工 在循环的下一次迭代中创建的对象,导致在指针中看到的交替模式。