指向同一地址的空指针

Void pointer pointing to the same address

问题

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

描述

我有一个简单的 class,我想通过将其转换为 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]

但是,如果我通过将引用计数器添加到 classes 列表来强制引用计数器:

[140663518040448, 140663518040472, 140663518040496]

您的对象最终位于同一地址这一事实纯属巧合。您的问题是当对它们的最后一个 python 引用消失时,您的 python 对象被销毁。如果你想让 python 对象保持活动状态,你需要在某处保存对它们的引用。

在您的情况下,由于 tmp 是您在循环中创建的 Temp 对象的唯一引用,因此每次重新分配 tmp 时,它之前的对象引用被破坏。这会在内存中留下空白 space,它的大小恰好适合保存在下一次​​循环迭代中创建的 Temp 对象,从而导致您在指针中看到的交替模式。

这个回答变得很长,所以快速浏览一下内容:

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

观察到的行为的解释

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

显然,对已创建对象的唯一引用是变量 tmp,一旦将其重新绑定到新创建的 Temp-object,旧对象的 reference-counter 将变为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)

现在所有对象都保持活动状态,并且 "die" 只有在显式调用 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 指针后立即增加 ref-counter,并在释放指针后立即减少它。
  2. 三规则意味着我们还必须注意复制构造函数和赋值运算符
  3. 我省略了 c++11 的 move-stuff,但你也需要注意它。

nogil 模式问题

但是有一件非常重要的事情:你不应该用上面的实现发布 GIL(即导入它为 PyObjectHolder(PyObject *o) nogil 但是当 C++ 时也有问题复制向量和类似的) - 因为否则 Py_XINCREFPy_XDECREF 可能无法正常工作。

为了说明这一点,让我们看一下下面的代码,它释放 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 个引用(sys.getrefcount 内的 +2)引用处于活动状态,因此该对象永远不会被摧毁。

使 PyObjectHolder 线程安全

那怎么办?最简单的解决方案是使用互斥锁来保护对引用计数器的访问(即每次调用 Py_XINCREFPy_XDECREF 时)。这种方法的缺点是它可能会显着降低单核代码的速度(例如,参见 this old article 关于用类似互斥锁的方法替换 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
    ...

现在,运行 从上面截取的代码产生 expected/right 行为:

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-threads,而不适用于 Python-interpreter 创建的线程。

这是互斥锁解决方案失败的示例。

首先,将 nogil-function 包装为 def-function:

%%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::mutex 的替代方法是使用 Python 机制,即 PyGILState_STATE,这将导致类似于

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

这也适用于上面的 threading-示例。然而,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)