Этот ответ стал довольно длинным, поэтому есть краткий обзор содержания:
- Объяснение наблюдаемого поведения
- Наивный подход во избежание проблемы
- Более систематическое и c ++ - типичное решение
- Иллюстрирование проблемы для многопоточного кода в режиме "nogil"
- Расширение c ++ - типичное решение для режима nogil
Объяснение наблюдаемого поведения
Сделка с 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
увеличивает ref-counter, как только он овладевает PyObject
-поинтером, и уменьшает его, как только освобождает указатель. - Правило трех означает, что мы также должны позаботиться о конструкторе копирования и операторе присваивания.
- Я опустил move-stuff для c ++ 11, но вы должны позаботиться об этом.также.
Проблемы с режимом 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 ссылок (+2 внутри sys.getrefcount
) ссылок, поэтому этот объект никогда не будет уничтожен.
Создание PyObjectHolder
поточно-ориентированного
Так что же делать?Самое простое решение - использовать мьютекс для защиты доступа к реф-счетчику (т.е. каждый раз, когда вызывается Py_XINCREF
или Py_XDECREF
).Недостатком этого подхода является то, что он может значительно замедлить одноядерный код (см., Например, эту старую статью о более старой попытке заменить 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-threads, но не для потоков, созданных 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::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)