python扩展——引用计数与内存泄露


背景
Python往往包含大量的内存分配和释放,同样需要避免内存泄漏(对于分配的内存忘记释放)和野指针(读写没有分配的内存区域)。他选择的方法就是 引用计数 。其原理比较简单:每个对象都包含一个计数器,计数器的增减与引用的增减直接相关,当引用计数为0时,表示对象已经没有存在的意义,可以删除了。在pyhton扩展中,C/C++语言来操作python对象的引用计数是一个非常有趣也非常容易出现错误的事情。对于拥有的引用,在不再需要时负责调用 Py_DECREF() 来减少引用计数。忘记减少拥有的引用计数会导致内存泄漏,忘记增加拥有的引用计数可能会导致解释器崩溃,那什么时候需要增减引用计数呢?
 
拥有权规则
任何人都无法拥有一个对象的所有权,但是可以拥有它的引用。
一个对象的引用无论何时传入或传出一个函数,它都是函数接口规范的一部分,而与所有权被转让与否无关。
大多数返回一个对象引用的函数通过引用转让所有权。尤其是那些功能就是创建新对象的函数,例如PyInt_FromLong()和Py_BuildValue(),会转让所有权给接收者。即使对象实际是不是新创建的,你接收到的仍然是对象的一个新的引用。例如:PyInt_FromLong()维护了常用值的一个缓存,能返回已缓存对象。
需要说明的另外一个概念叫做“借用”对象,顾名思义,借用的对象不需要对它的引用计数加1,所以更不能调用 Py_DECREF() 来减少引用计数。借用的优点是你无需管理引用计数,缺点是可能被野指针搞的头晕,因为你并不能保证借用的这个对象在使用时是不是已经被释放了。PyTuple_GetItem()、PyList_GetItem()、PyDict_GetItem()、和PyDict_-GetItemString()都是返回你从元组、列表或字典借得的引用。
如果一个对象引用被传入另一个函数,一般而言,是函数从你那借用引用——如果函数需要存储引用的话,它要使用Py_INCREF()以成一个独立的所有者。对这一规则,有两个重要的例外:PyTuple_SetItem()和PyList_SetItem()。这两个函数接管传给它们的子项的所有权——即使这些子项失效了!(PyDict_SetItem()那类函数不接管所有权)
当一个C函数在Python中被调用,它从调用者那里借用自已参数的引用。调用者拥有对象的一个引用,所以“借引用”的寿命在返回值退出之前都有保证。仅当这样的“借引用”要被存储或传递时,才要通过调用Py_INCREF()来转换成“拥有引用”。
在Python调用的C函数返回的对象引用必须是“拥有引用”——所有权由函数转让给了它的调用者
 
常见的一些错误
明显的引用错误
PyObject*_list = PyList_New( 0 );                                                                                                                                                                   
PyObject*_list1 = PyList_New( 0 );
return_list;
 
由于_list1被创建后,既没有被释放(调用Py_DECREF())也没有返回(return转让所有权),所以导致内存泄露。
 
PyList_Append()
(这个函数说多了都是泪,被他折腾了半天才查到内存泄露的原因。)
一些情况下,扩展函数需要向python返回一个list,于是会有这种写法:
 
PyObject*_list = PyList_New( 0 );
 
Intvar = …;
PyList_Append(_list,PyInt_FromLong(var);
Return_list;
 
看上去并没什么毛病,但是追查一番发现问题出现在PyList_Append身上,他会对第二个参数的引用计数加1,在这个例子中PyInt_FromLong(var)返回一个对象,它的引用是1,PyList_Append把这个对象加入到list后,引用计数为2。之后将_list拥有权转交给python调用方。后面可想而知了,调用方在释放这个List的时候只会对计数减一,也就是说该对象计数永远无法到0(也就意味着内存泄露了)。
 
解决的办法有两个:
 
办法1
PyObject*_list = PyList_New( 0 );
Int var= …;
PyObject * tmp = PyInt_FromLong(var);
PyList_Append(_list,tmp);
Py_DECREF(jpo);
Return_list;
 
办法2
PyObject*_list = PyList_New( CNT );
Int var= …;
PyList_SetItem(_list,0, PyInt_FromLong(var));
Return_list;
 
其中CNT表示指定list的长度,然后依次去对每一个下标元素赋值,而不是再采用APPEND的方法。至于为何PyList_SetItem可以解决这个问题,可以参加“拥有权规则哪里的介绍”
 

相关内容

    暂无相关文章

评论关闭