这个答案变得相当长,因此对内容进行了快速概述:
-
观察行为的解释
-
避免问题的天真方法
-
一个更系统的C++典型解决方案
-
说明“nogil”模式下多线程代码的问题
-
扩展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
值得注意的是:
-
PyObjectHolder
一旦拥有一个
PyObject
-指针,并在释放指针后立即将其减小。
-
三条规则意味着我们还必须注意复制构造函数和赋值运算符
-
我已经省略了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)