有少数情况下,借用的引用看起来无害,但却可能导致问题。这通常是因为解释器的隐式调用,并可能导致引用拥有者处置这个引用。
首先需要特别注意的情况是使用 Py_DECREF() 到一个无关对象,而这个对象的引用是借用自一个列表的元素。举个实例:
void
bug(PyObject *list)
{
PyObject *item = PyList_GetItem(list, 0);
PyList_SetItem(list, 1, PyLong_FromLong(0L));
PyObject_Print(item, stdout, 0); /* BUG! */
}
这个函数首先借用一个引用 list[0] ,然后替换 list[1] 为值 0 ,最后打印借用的引用。看起来无害是吧,但却不是。
让我们跟随控制流进入 PyList_SetItem()。 列表拥有对其所有条目的引用,因此当条目 1 被替换时,它必须丢弃原始条目 1。 现在我们假设原始条目 1 是一个用户定义类的实例,并进一步假设该类定义了一个 __del__() 方法。 如果该类实例的引用计数为 1,那么丢弃它时将调用其 __del__() 方法。
由于它是用 Python 编写的,因此 __del__() 方法可以执行任意 Python 代码。 它是否可以使 bug() 中对 item 的引用失效呢? 当然可以! 假定传入 bug() 的列表可以被 __del__() 方法访问,它就可以执行一条语句实现 del list[0] 的效果,假定这是对该对象的最后一次引用,它就会释放与之相关联的内存,从而使 item 失效。
解决方法是,当你知道了问题的根源,就容易了:临时增加引用计数。正确版本的函数代码如下:
void
no_bug(PyObject *list)
{
PyObject *item = PyList_GetItem(list, 0);
Py_INCREF(item);
PyList_SetItem(list, 1, PyLong_FromLong(0L));
PyObject_Print(item, stdout, 0);
Py_DECREF(item);
}
这是一个真实的故事。 一个较旧版本的 Python 曾经包含此问题的变化形式,有人在 C 语言调试器中花费了大量时间,才弄明白为什么他的 __del__() 方法会失败……
这个问题的第二种情况是借用的引用涉及线程的变种。通常,Python解释器里多个线程无法进入对方的路径,因为有个全局锁保护着Python整个对象空间。但可以使用宏 Py_BEGIN_ALLOW_THREADS 来临时释放这个锁,重新获取锁用 Py_END_ALLOW_THREADS 。这通常围绕在阻塞I/O调用外,使得其他线程可以在等待I/O期间使用处理器。显然,如下函数会跟之前那个有一样的问题:
void
bug(PyObject *list)
{
PyObject *item = PyList_GetItem(list, 0);
Py_BEGIN_ALLOW_THREADS
...some blocking I/O call...
Py_END_ALLOW_THREADS
PyObject_Print(item, stdout, 0); /* BUG! */
} |