Python(cpython)在内存屏障和原子性方面的行为是否有保证?

6

我在想Java中的“volatile”在Python中的等价物是什么,结果找到了这个答案。

An equivalent to Java volatile in Python

该答案(基本上)表示,在CPython中,所有东西都是有效的,因为有全局解释器锁(GIL)。 这很有道理,因为GIL锁定了所有东西,无需担心内存屏障等。 但是,如果这是规范文件记录和保证的结果,而不是CPython当前实现的结果,我会更加满意。

比如说,我想让一个线程发布数据,其他线程读取它,我可以选择像这样:

class XFaster:
    def __init__(self):
        self._x = 0

    def set_x(self, x):
        self._x = x

    def get_x(self, x):
        return self._x


class XSafer:
    def __init__(self):
        self._x = 0
        self._lock = threading.Lock()

    def set_x(self, x):
        with self._lock:
            self._x = x

    def get_x(self, x):
        with self._lock:
            return self._x

我宁愿使用XFaster,甚至不使用getter和setter。但我也想要可靠和“正确”的操作。是否有一些官方文档表明这是可以的?比如将值放入dict或向list追加内容?

换句话说,是否有一种系统化的、有文档支持的方法来确定我可以在没有threading.Lock的情况下做什么(而不需要挖掘dis之类的东西)?并且最好不会在未来的Python版本中出现问题。

编辑:感谢评论区中的讨论。但我真正想要的是一些规范,保证以下内容:

如果我执行类似于以下的代码:

# in the beginning
x.a == foo
# then two threads start

# thread 1:
x.a = bar

# thread 2
do_something_with(x.a)

我希望确保:

  • 当线程2读取x.a时,它读取的是foobar之一。
  • 如果线程2中的读取在物理上晚于线程1中的赋值发生,则实际上读取的是bar

以下是我不希望发生的情况:

  • 线程被调度到不同的处理器上,并且线程1中的赋值x.a=bar对线程2不可见。
  • x.__dict__正在重新哈希过程中,因此线程2读取了垃圾数据。
  • 等等

1
@PeterCordes - 相反的情况才是真的。 CPython的字节码解释器是单线程的,由全局解释器锁定控制。在持有锁定的同时,运行任何给定的字节码操作并且与其他字节码操作相对应是原子性的,直到完成。例外情况是当操作导致调用特定释放锁定的扩展函数(最可能为C语言编写)时。 - tdelaney
@大家好,这些都是很好的观点,但是这次讨论说明了我为什么需要一个可靠的文档规范,而不是基于cpython实现的推断。 - mrip
1
@PeterCordes 这并没有完全回答这个问题。首先,它取决于“单个语句”的定义是什么(例如,a,b=c,d 是一个单独的语句吗?)。但也仅仅因为它们不同时发生,并不意味着线程 A 看到的内存将反映线程 B 中发生的所有事情,比如操作系统在不同处理器上安排线程的情况。它应该,而且几乎肯定会工作,但如果 python.org 上有一些明确说明的文档,那将是很好的。 - mrip
1
“物理上晚一些”是什么意思?通常在讨论多线程算法时,我们不会使用全局可访问的时钟等东西。 - Davis Herring
1
@MisterMiyagi,我想问一下在哪里可以找到官方文档告诉我何时需要使用显式锁定以及何时不需要。例如,在我上面定义的类中,我是否可以从一个线程调用set_x,并从其他线程使用XFaster调用get_x,或者我需要使用XSafer来确保我不会遇到任何问题。而且我想在python.org上阅读相关内容,而不是在某个博客文章上。 - mrip
显示剩余22条评论
1个回答

3
TLDR:CPython 保证其自身的数据结构对于损坏是线程安全的。这并不意味着任何自定义数据结构或代码都是无竞争状态的。
GIL的目的是保护CPython的数据结构免受损坏。人们可以依赖于其内部状态是线程安全的。
“全局解释器锁(Python文档-词汇表)”机制是CPython解释器用来确保一次只有一个线程执行Python字节码的方法。这简化了CPython的实现,因为它使对象模型(包括关键内置类型,如dict)隐式地安全防止并发访问。这也意味着跨线程的更改具有正确的可见性。
但是,这并不意味着任何孤立的语句或表达式都是原子的:几乎任何语句或表达式都可以调用多个字节码指令。因此,对于这些情况,GIL明确地不提供原子性。

具体而言,像 x.a=bar 这样的语句可能通过调用 setter 通过 object.__setattr__描述符协议 执行任意多个字节码指令。它至少执行三个字节码指令,包括 bar 查找、x 查找和 a 赋值。

因此,Python 保证 可见性/一致性,但不提供对竞争条件的任何保证。如果一个对象同时被修改,必须进行同步以确保正确性。


谢谢。接受是因为这可能是最好的了。如果他们列出了“关键内置类型”(列表、集合、64位以上的整数、多重赋值等等),那就太好了。是的,它似乎暗示了正确的内存可见性,尽管他们明确说明也不会有什么坏处。我知道这似乎让人感到痛苦,但我真正想要的只是一个规范,这样我就可以编写能够正常工作的代码并且充满信心。 - mrip
@mrip 我理解你的担忧,以及对缺乏明确规范的沮丧。然而,就所有意图和目的而言,Python和CPython都不保证原子性。即使是内置类型也只能保证不会被破坏 - 但不能保证它们的操作是原子的。任何一组并发操作对象必须进行同步才能正确执行。 - MisterMiyagi

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