【问题标题】:Is Python variable assignment atomic?Python变量赋值是原子的吗?
【发布时间】:2011-01-18 11:07:40
【问题描述】:

假设我正在使用signal 处理程序来处理间隔计时器。

def _aHandler(signum, _):
  global SomeGlobalVariable
  SomeGlobalVariable=True

我能否设置SomeGlobalVariable 而不必担心,在不太可能的情况下,在设置SomeGlobalVariable(即Python VM 正在执行字节码来设置变量)时,信号处理程序中的赋值会破坏某些东西? (即元稳定状态)

更新:我对在处理程序之外进行“复合分配”的情况特别感兴趣。

(也许我想得太“低级”了,而这一切都在 Python 中处理...来自嵌入式系统背景,我不时有这种冲动)

【问题讨论】:

    标签: python signals


    【解决方案1】:

    简单变量的简单赋值是“原子的”AKA线程安全(复合赋值,如+=或对象的项目或属性的赋值不需要,但你的例子是一个简单的,尽管是全局变量的简单赋值,因此安全)。

    【讨论】:

    • 如果处理程序确实(例如)gvar = 3gvar 最初是 7,并且处理程序之外的代码确实(例如)gvar += 2,那么 gvar 最终可能是3、5 或 9,具体取决于操作最终交错的方式。这在技术上是“安全的”(意思是进程不会崩溃;-)但在语义上不太可能正常。
    • 这是在哪里指定的? -1 缺乏权威参考。
    • 官方文档将被视为权威,我会说。如果没有记录,那么它是依赖于实现的,不是吗?
    • @R.MartinhoFernandes,绝对依赖于实现——当然,在现实世界中,CPython 参考实现是如此占主导地位,以至于所有其他人大多试图坚持其(记录或未记录的)行为,使得唯一的理论和迂腐相关的问题之一。鉴于此,我认为在主导实施的现实世界中用“实际发生的事情”来回答这个问题,与胡扯或拒绝提供帮助相比,仍然具有积极的价值,即使不存在“权威参考”(也不太可能出现任何很快)。
    • 如果标准没有明确声明简单赋值是原子的,那么答案应该提到行为取决于实现。现在的答案是在不引用任何权威来源的情况下提出强有力的主张,所以 -1。
    【解决方案2】:

    Google 的样式指南建议不要这样做

    我并不是说 Google 风格指南是终极真理,但 rationale in the "Threading" section 提供了一些见解(重点是我的):

    不要依赖内置类型的原子性。

    虽然 Python 的内置数据类型(如字典)似乎具有原子操作,但在某些极端情况下它们不是原子的(例如,如果 __hash____eq__ 被实现为 Python 方法)并且它们的原子性不应该可以依靠。 您也不应该依赖原子变量赋值(因为这又取决于字典)。

    使用Queue 模块的队列数据类型作为线程间数据通信的首选方式。否则,请使用 threading 模块及其锁定原语。了解条件变量的正确使用,以便您可以使用threading.Condition 而不是使用较低级别的锁。

    所以我的解释是,在 Python 中,一切都类似于 dict,当您在后端执行 a = b 时,globals['a'] = b 正在发生,这很糟糕,因为 dicts 不一定是线程安全的。

    对于单个变量,Queue 并不理想,因为我们希望它只包含一个元素,而且我在标准库中找不到完美的预先存在的容器来自动同步 .set() 方法。所以现在我只做:

    import threading
    
    myvar = 0
    myvar_lock = threading.Lock()
    with myvar_lock:
        myvar = 1
    with myvar_lock:
        myvar = 2
    

    有趣的是Martelli does not seem to mind那个谷歌风格指南推荐:-)(他在谷歌工作)

    我想知道 CPython GIL 是否对这个问题有影响:What is the global interpreter lock (GIL) in CPython?

    该线程还表明 CPython dicts 是线程安全的,包括以下明确提到它的词汇表引用 https://docs.python.org/3/glossary.html#term-global-interpreter-lock

    这通过使对象模型(包括关键的内置类型,如 dict)对并发访问隐式安全,从而简化了 CPython 实现。

    【讨论】:

    • 我有点怀疑风格指南的断言,即变量赋值可能不是原子的,因为它依赖于字典。当键不是内置类型时,字典操作可能不是原子的,但对于变量赋值,键是字符串(内置类型)。
    • @CS 感谢您的反馈。虽然这对于 CPython 来说是正确的,但我遵循该建议的主要原因是期待其他可能没有 GIL 的实现。
    • 看最后一句话,是否可以断定任何生成单个字节码指令的语句都是线程安全的?
    • 我对 OP 的原始示例特别感兴趣:处理信号。如果 Python 信号处理函数可以随时在主线程上运行,你如何保护主线程免受竞争条件的影响。据我所知,使用锁和朋友在这里无济于事,因为我们在同一个线程中,只会立即死锁。
    【解决方案3】:

    您可以尝试dis 查看底层字节码。

    import dis
    
    def foo():
        a = 1
    
    
    dis.dis(foo)
    
    

    产生字节码:

    # a = 1
    5             0 LOAD_CONST               1 (1)
                  2 STORE_FAST               0 (a)
    

    所以赋值是一个单独的python字节码(指令2),它在CPython中是原子的,因为它一次执行一个字节码。

    而,添加一个a += 1

    def foo():
        a = 1
        a += 1
    

    产生字节码:

    # a+=1
    6             4 LOAD_FAST                0 (a)
                  6 LOAD_CONST               1 (1)
                  8 INPLACE_ADD
                 10 STORE_FAST               0 (a)
    

    +=对应4条指令,不是原子的。

    【讨论】:

      【解决方案4】:

      复合赋值涉及三个步骤:读-更新-写。如果另一个线程在读取发生之后但在写入之前运行并将新值写入该位置,则这是一种竞争条件。在这种情况下,一个陈旧的值被更新并写回,这将破坏另一个线程写入的任何新值。在 Python 中,任何涉及执行单个字节码的东西都应该是原子的,但复合赋值不符合这个标准。使用锁。

      【讨论】:

      • 在上面描述的情况下,我只有一个执行线程。此外,我不能“延迟”信号处理程序的执行。当然,如果 SO 的智囊团意见如此规定,我可以求助于线程安全队列。
      • 如果你有一个单线程,处理程序在哪里运行?如果它在同一个线程上,那么在它运行时没有任何东西可以改变状态。
      • @Max S.:你确定?看看@Alex Martelli 的回答。
      • jldupont,很确定。亚历克斯的回答非常好,但它只适用于你有多个控制线程的情况;只有一名赛车手时没有比赛条件。
      • @Max S.:但在这种情况下,我相信我们还有另一个赛车手:“信号调度代理”……我看不到任何信号轮询。当然,我可能完全没吃午饭了:我想看看相关文档来支持你的情况。
      猜你喜欢
      • 2011-02-13
      • 2019-05-05
      • 2023-01-26
      • 2014-08-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多