这个答案变得很长,所以有一个内容的快速概述:
- 对观察到的行为的解释
- 避免问题的简单方法
- 更系统化和 c++ 典型的解决方案
- 说明“nogil”模式下多线程代码的问题
- 为 nogil 模式扩展 c++ 典型解决方案
对观察到的行为的解释
与 Cython 的交易:只要您的变量是 object 类型或从它继承(在您的情况下为 cdef Temp),cython 就会为您管理引用计数。只要您将其转换为 PyObject * 或任何其他指针 - 引用计数就是您的责任。
显然,对创建对象的唯一引用是变量tmp,一旦你将它重新绑定到新创建的Temp-对象,旧对象的引用计数器变为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)
现在所有对象只有在显式调用 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
值得注意的事情:
-
PyObjectHolder 在拥有 PyObject 指针后立即增加 ref-counter,并在释放指针后立即减少它。
- 三法则意味着我们还必须注意复制构造函数和赋值运算符
- 我已经省略了 c++11 的 move-stuff,但您也需要注意它。
nogil 模式的问题
然而有一件非常重要的事情:你不应该使用上述实现发布 GIL(即,将其导入为 PyObjectHolder(PyObject *o) nogil,但当 C++ 复制向量和类似内容时也会出现问题) -因为否则Py_XINCREF 和Py_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 线程安全
那该怎么办?最简单的解决方案是使用互斥锁来保护对 ref-counter 的访问(即每次调用 Py_XINCREF 或 Py_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
...
现在,运行从上面截取的代码会产生预期/正确的行为:
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-function 包装为 def-function:
%%cython --cplus -c=/openmp
...
def single_create_vectors(object o):
cdef PyObject *ptr=<PyObject *>o
with nogil:
create_vectors(ptr)
现在使用threading-module 来创建
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-example。然而,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)