Python属性/属性是否线程安全?

4
我有如下代码:
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 这样的语句上都存在可能的竞态条件,但我很少需要执行这些操作,因此我会在必要时添加解决方法。

谢谢!


errors 属性的 getter 没有正确地缩进。 - Niklas R
好的,你说得对。那是我在 StackOverflow 上重新格式化时犯的错误 :P - morrog
1个回答

12

你可能需要更深入地思考什么是线程安全。考虑一下,如果你写了以下代码:

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

这段代码与您发布的代码完全一样安全。在Python中,将值分配给属性是线程安全的:该值总是被分配;它可能会被来自另一个线程的另一个赋值覆盖,但您始终会获得其中一个值,而不是两个值的混合。同样地,访问属性会给您提供当前值。

您的代码失败的原因是,正如您所说,对于您的原始版本或简化版本,以下行:

shared.errors += 1

不是线程安全的,但这正是让你编写安全代码的全部意义,这些是你需要注意的事情,而不是简单的get/set。

回答评论中的问题:

在Python中,简单的赋值操作只是重新绑定名称(而不是创建副本),并且保证是原子的;您将获得一个值或另一个值。然而,对属性(或下标变量)的赋值可能会被覆盖,就像上面的属性一样。在这种情况下,属性赋值有可能会出错。因此,答案是属性赋值通常是安全的,但如果已被覆盖为属性或setattr等,则不安全。

此外,如果要替换的旧值是带有析构函数的Python类或包含带有析构函数的Python类的内容,则析构函数代码可能会在赋值过程中运行。Python仍然会确保其自己的数据结构不会被损坏(因此您永远不应该遇到segfault),但它不会为您的数据结构做同样的事情。显而易见的解决方法是永远不要在您的任何类上定义del。


谢谢你的回答!快速问题:在Python中,如果两个线程随意写入对象属性,并且第三个线程读取相同的属性,那么第三个线程会得到正确的结果吗?要么是线程1写入的内容,要么是线程2写入的内容,但不会出现中间值(字节混合或垃圾)?我正在重构旧代码,我认为第一次编写时我太过谨慎。我不关心多核缓存问题或其他可能使第三个线程获取旧值的问题,只要它得到的值是正确的即可。 - morrog
我在我的主要回答中回答了你的评论。 - Duncan

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接