LinuxSir.cn,穿越时空的Linuxsir!

 找回密码
 注册
搜索
热搜: shell linux mysql
查看: 309|回复: 0

危险的薄冰

[复制链接]
发表于 2024-1-17 17:15:00 | 显示全部楼层 |阅读模式


有少数情况下,借用的引用看起来无害,但却可能导致问题。这通常是因为解释器的隐式调用,并可能导致引用拥有者处置这个引用。

首先需要特别注意的情况是使用 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! */
}
您需要登录后才可以回帖 登录 | 注册

本版积分规则

快速回复 返回顶部 返回列表