线程安全的Python属性?

4 投票
1 回答
5358 浏览
提问于 2025-04-16 22:57

我有一段代码,像下面这样:

class SomeSharedData(object):
    def __init__(self):
        self._lock = RLock()
        self._errors = 0

    @property
    def errors(self):
        with self._lock:
        return self._errors

    @errors.setter
    def errors(self, value):
        with self._lock:
            self._errors = value

除了还有更多的属性,比如 errors。我这里的目标是让使用起来更简单,而不是追求效率,所以多一些锁定也没关系。

有没有更简洁的方法来定义这些属性呢?

到目前为止,我想到的最好办法是这个:

class thread_safe_property(object):
    def __init__(self, name=None):
        self.name = name

    def __get__(self, obj, objtype):
        with obj._lock:
            return getattr(obj, self.name)

    def __set__(self, obj, val):
        with obj._lock:
            setattr(obj, self.name, val)

class SomeSharedData(object):
    def __init__(self):
        self._lock = RLock()
        self._errors = 0

    errors = thread_safe_property('_errors')

有什么想法?更好的方法吗?

原来的代码和新的方法都有可能出现竞争条件,比如在 data.errors += 1 这样的语句上,但我很少需要执行这些操作,所以在需要的地方我会添加一些解决办法。

谢谢!

1 个回答

12

你可能需要更仔细地想想“线程安全”到底是什么意思。想象一下,如果你写了下面这段代码:

class SomeSharedData(object):
    def __init__(self):
        self.errors = 0

这段代码的“线程安全”程度和你之前发的代码是完全相同的。在Python中,给一个属性赋值是线程安全的:这个值总是会被赋上去;虽然它可能会被另一个线程赋的值覆盖,但你总是能得到一个值,而不会得到两个值混在一起的情况。同样,访问这个属性时,你得到的也是当前的值。

你的代码出问题的地方在于,无论是你原来的代码还是简化版,像下面这样的行:

shared.errors += 1

并不是线程安全的,但这正是让你的代码安全的关键所在,这些是你需要注意的地方,而不是简单的获取或设置值。

针对评论中的问题:

在Python中,简单的赋值实际上是重新绑定一个名字(而不是复制),这是保证原子性的;你要么得到一个值,要么得到另一个值。然而,给一个属性(或者带下标的变量)赋值可能会被覆盖,就像你上面提到的属性那样。在这种情况下,属性赋值可能会出问题。所以答案是,属性赋值通常是安全的,但如果它被属性或setattr等覆盖了,就不一定安全了。

另外,如果被替换的旧值是一个有析构函数的Python类,或者是包含有析构函数的Python类的东西,那么析构函数的代码可能会在赋值的过程中运行。Python会确保它自己的数据结构不会被破坏(所以你不会遇到段错误),但对于你自己的数据结构就不一定了。解决这个问题的明显方法是,绝对不要在你的任何类中定义del

撰写回答