CPython 教程为自定义类型定义了一个自定义初始值设定项,其中包含以下几行:
if (first) {
tmp = self->first;
Py_INCREF(first);
self->first = first;
Py_XDECREF(tmp);
}
但是教程建议不要使用这个更简单但更邪恶的版本:
if (first) {
Py_XDECREF(self->first);
Py_INCREF(first);
self->first = first;
}
原因如下:
但这会有风险。我们的类型不限制第一个成员的类型,因此它可以是任何类型的对象。它可能有一个析构函数,导致执行尝试访问第一个成员的代码;或者该析构函数可以释放全局解释器锁并让任意代码在访问和修改我们的对象的其他线程中运行。
我明白了多线程编程能够打破简单版本的原因。可能存在竞争条件,其中 new
self->first
由一个线程设置并由另一个线程释放(通过 Py_XDECREF
)。在正确的版本中可以避免这种情况,因为两个线程都必须在释放它之前设置 self->first
。
我不明白的部分是这一部分:
它可能有一个析构函数,导致执行尝试访问第一个成员的代码如果
first
是一个正在被垃圾回收的对象,则必须调用其析构函数,并且它应该可以在析构函数期间访问自身。为什么这会很危险?谢谢!
custom = Custom(...) # global variable
class SomePyClass:
def __del__(self):
# access global variable
custom.__init__(1, 2, 3)
假设 custom.first
是
SomePyClass
的实例,并且该实例在
Py_XDECREF(self->first);
行被破坏。析构函数中的任意代码将导致
custom.__init__
再次被调用。这将导致
Py_XDECREF
在一个确实应该有 0 的引用计数并且已经在被销毁的过程中的对象上被再次调用。仅此一点就代表了引用计数错误,因为它的引用计数最终将低于 0(请注意,这可能并不完全是发生的情况,因为在析构函数期间它暂时获得了 1 的引用计数,但它绝对是一个引用-计数错误)。另请注意,在
first
中分配给
__init__
的新值也是错误引用计数的:虽然我们的实例被减值两次,但即将分配的值将被替换,而不会减值。