Python C API引用计数问题引发随机读访问违规求助
我有一段嵌入大型原生Python项目的C++代码,会触发多种随机读访问违规。怀疑是引用计数处理存在问题,但无法定位。代码结构如下:
typedef struct { PyObject_HEAD MyCustomClass *self; } PyMyCustomClass; class MyCustomClass { public: PyObject *values; PyObject *incr_values; ... }
两个属性均初始化为None元组,MyCustomClass包含以下方法:
MyCustomClass(){ values = Py_BuildValue(""); incr_values= Py_BuildValue(""); } ~MyCustomClass(){ Py_DECREF(this->values); Py_DECREF(this->incr_values); } PyObject *get_values() { Py_INCREF(this->values); return this->values; } int set_incr_values( PyObject *new_values) { Py_DECREF(this->incr_values); Py_INCREF(new_values); this->incr_values = new_values; return 0; } PyObject *compute_incr_values() { if( condition ) return this->get_values(); //new reference else { //add 1 to all values PyObject *one = Py_BuildValue("i", 1); Py_ssize_clean_t size = PyTuple_GET_SIZE(this->values); PyObject *new_values = PyTuple_New(size); for(Py_ssize_t i = 0; i < size; i++ ) { PyObject *item = PyTuple_GET_ITEM(input,i); auto add_fct = Py_TYPE(item)->tp_as_number->nb_add; PyTuple_SET_ITEM(new_values, i, add_fct(item,one) ); } Py_DECREF(one); return new_values; //new reference } } static PyObject *compute_incr_values(PyMyCustomClass *obj, PyObject *Py_UNUSED) { PyObject *new_values = obj->self->compute_incr_values(); obj->self->set_incr_values(new_values); Py_DECREF(new_values); //Get rid of unused object Py_RETURN_NONE; }
当前代码会在Python中触发随机读访问违规,但移除析构函数中的Py_DECREF(this->values);和compute_incr_values方法中的Py_DECREF(new_values);后即可正常运行。无法理解问题所在,请问是否是引用计数处理存在问题?
你的代码存在多处引用计数错误,正是这些问题导致了随机读访问违规:
1. None对象的引用计数错误
Py_BuildValue("")返回的是全局单例的None对象,Python规定永远不能对None调用Py_DECREF。你在构造函数中给values和incr_values赋值为None,之后在析构函数中调用Py_DECREF,这会错误减少None的引用计数,触发内存访问异常。
修复:直接使用Py_None,并手动增加引用计数(因为要持有该对象的引用):
MyCustomClass(){ Py_INCREF(Py_None); values = Py_None; Py_INCREF(Py_None); incr_values = Py_None; }
这样析构函数中的Py_DECREF操作就安全了,因为我们正确持有了None的引用。
2. 新旧值相同时的双重释放问题
当condition为真时,compute_incr_values返回的是values的新引用,随后在Python绑定函数中调用set_incr_values:
set_incr_values会先对旧的incr_values调用Py_DECREF,如果旧值恰好和新值(也就是values)是同一个对象,这会直接减少values的引用计数- 接着对新值调用
Py_INCREF,引用计数恢复 - 最后外面的
Py_DECREF(new_values)又会减少values的引用计数
如果此时values的引用计数降到0,对象会被提前销毁,后续析构函数再调用Py_DECREF(this->values)就会导致重复释放,触发访问违规。
修复:在set_incr_values中先判断新旧值是否相同,相同则跳过操作:
int set_incr_values( PyObject *new_values) { if (this->incr_values == new_values) { return 0; } Py_DECREF(this->incr_values); Py_INCREF(new_values); this->incr_values = new_values; return 0; }
这样就能避免新旧值相同时的错误释放操作。
3. 循环中的笔误隐患
循环里的PyTuple_GET_ITEM(input,i)应该是PyTuple_GET_ITEM(this->values,i)吧?如果是笔误,会导致访问无关内存,这也可能触发随机异常,需要修正。
4. GIL持有的问题
Python的引用计数操作不是线程安全的,必须在持有GIL(全局解释器锁)时执行。如果MyCustomClass的析构函数是在GIL未持有时调用的,Py_DECREF会导致线程安全问题。需要确保MyCustomClass的销毁是在GIL持有期间进行,比如在PyMyCustomClass的tp_dealloc函数中先获取GIL,再销毁obj->self。
内容的提问来源于stack exchange,提问作者m3shoot

