Python 中的 += 运算符是线程安全的吗?

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/1717393/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-11-03 22:55:32  来源:igfitidea点击:

Is the += operator thread-safe in Python?

pythonthread-safetyincrement

提问by nubela

I want to create a non-thread-safe chunk of code for experimentation, and those are the functions that 2 threads are going to call.

我想为实验创建一个非线程安全的代码块,这些是 2 个线程将要调用的函数。

c = 0

def increment():
  c += 1

def decrement():
  c -= 1

Is this code thread safe?

这段代码线程安全吗?

If not, may I understand why it is not thread safe, and what kind of statements usually lead to non-thread-safe operations.

如果不是,我是否可以理解为什么它不是线程安全的,以及通常会导致非线程安全操作的语句。

If it is thread-safe, how can I make it explicitly non-thread-safe?

如果它是线程安全的,我怎样才能使它明确地非线程安全?

采纳答案by Jochen Ritzel

Single opcodes are thread-safe because of the GIL but nothing else:

由于 GIL,单个操作码是线程安全的,但没有别的:

import time
class something(object):
    def __init__(self,c):
        self.c=c
    def inc(self):
        new = self.c+1 
        # if the thread is interrupted by another inc() call its result is wrong
        time.sleep(0.001) # sleep makes the os continue another thread
        self.c = new


x = something(0)
import threading

for _ in range(10000):
    threading.Thread(target=x.inc).start()

print x.c # ~900 here, instead of 10000

Everyresource shared by multiple threads musthave a lock.

多个线程共享的每个资源都必须有一个锁。

回答by Glenn Maynard

No, this code is absolutely, demonstrably not threadsafe.

不,这段代码绝对是,显然不是线程安全的。

import threading

i = 0

def test():
    global i
    for x in range(100000):
        i += 1

threads = [threading.Thread(target=test) for t in range(10)]
for t in threads:
    t.start()

for t in threads:
    t.join()

assert i == 1000000, i

fails consistently.

持续失败。

i += 1 resolves to four opcodes: load i, load 1, add the two, and store it back to i. The Python interpreter switches active threads (by releasing the GIL from one thread so another thread can have it) every 100 opcodes. (Both of these are implementation details.) The race condition occurs when the 100-opcode preemption happens between loading and storing, allowing another thread to start incrementing the counter. When it gets back to the suspended thread, it continues with the old value of "i" and undoes the increments run by other threads in the meantime.

i += 1 解析为四个操作码:加载 i,加载 1,将两者相加,然后将其存储回 i。Python 解释器每 100 个操作码切换活动线程(通过从一个线程释放 GIL 以便另一个线程可以拥有它)。(这两个都是实现细节。)当 100-opcode 抢占发生在加载和存储之间时,就会出现竞争条件,从而允许另一个线程开始递增计数器。当它返回到挂起的线程时,它继续使用旧值“i”并同时撤消其他线程运行的增量。

Making it threadsafe is straightforward; add a lock:

使其成为线程安全的很简单;添加锁:

#!/usr/bin/python
import threading
i = 0
i_lock = threading.Lock()

def test():
    global i
    i_lock.acquire()
    try:
        for x in range(100000):
            i += 1
    finally:
        i_lock.release()

threads = [threading.Thread(target=test) for t in range(10)]
for t in threads:
    t.start()

for t in threads:
    t.join()

assert i == 1000000, i

回答by bobince

(note: you would need global cin each function to make your code work.)

(注意:您需要global c在每个函数中使您的代码工作。)

Is this code thread safe?

这段代码线程安全吗?

No. Only a single bytecode instruction is ‘atomic' in CPython, and a +=may not result in a single opcode, even when the values involved are simple integers:

不。在 CPython 中只有一个字节码指令是“原子的”,并且 a+=可能不会产生单个操作码,即使涉及的值是简单的整数:

>>> c= 0
>>> def inc():
...     global c
...     c+= 1

>>> import dis
>>> dis.dis(inc)

  3           0 LOAD_GLOBAL              0 (c)
              3 LOAD_CONST               1 (1)
              6 INPLACE_ADD         
              7 STORE_GLOBAL             0 (c)
             10 LOAD_CONST               0 (None)
             13 RETURN_VALUE        

So one thread could get to index 6 with c and 1 loaded, give up the GIL and let another thread in, which executes an incand sleeps, returning the GIL to the first thread, which now has the wrong value.

因此,一个线程可以在加载了 c 和 1 的情况下到达索引 6,放弃 GIL 并让另一个线程进入,该线程执行 aninc并休眠,将 GIL 返回到第一个线程,该线程现在具有错误的值。

In any case, what's atomic is an implementation detail which you shouldn't rely on. Bytecodes may change in future versions of CPython, and the results will be totally different in other implementations of Python that do not rely on a GIL. If you need thread safety, you need a locking mechanism.

在任何情况下,原子是您不应该依赖的实现细节。字节码在 CPython 的未来版本中可能会发生变化,结果在其他不依赖 GIL 的 Python 实现中将完全不同。如果需要线程安全,则需要锁定机制。

回答by gillesv

To be sure I recommend to use a lock:

可以肯定的是,我建议使用锁:

import threading

class ThreadSafeCounter():
    def __init__(self):
        self.lock = threading.Lock()
        self.counter=0

    def increment(self):
        with self.lock:
            self.counter+=1


    def decrement(self):
        with self.lock:
            self.counter-=1

The synchronized decorator can also help to keep the code easy to read.

同步装饰器还可以帮助保持代码易于阅读。

回答by John La Rooy

It's easy to prove that your code is not thread safe. You can increase the likelyhood of seeing the race condition by using a sleep in the critical parts (this simply simulates a slow CPU). However if you run the code for long enough you should see the race condition eventually regardless.

很容易证明您的代码不是线程安全的。您可以通过在关键部分使用睡眠来增加看到竞争条件的可能性(这只是模拟慢速 CPU)。但是,如果您运行代码足够长的时间,无论如何您最终都会看到竞争条件。

from time import sleep
c = 0

def increment():
  global c
  c_ = c
  sleep(0.1)
  c = c_ + 1

def decrement():
  global c
  c_ = c
  sleep(0.1)
  c  = c_ - 1

回答by ebo

Short answer: no.

简短的回答:没有。

Long answer: generally not.

长答案:一般不会。

While CPython's GIL makes single opcodes thread-safe, this is no general behaviour. You may not assume that even simple operations like an addition is a atomic instruction. The addition may only be half done when another thread runs.

虽然 CPython 的 GIL 使单个操作码线程安全,但这不是一般行为。您可能不会认为即使是像加法这样的简单操作也是原子指令。当另一个线程运行时,添加可能只完成了一半。

And as soon as your functions access a variable in more than one opcode, your thread safety is gone. You can generate thread safety, if you wrap your function bodies in locks. But be aware that locks may be computationally costly and may generate deadlocks.

一旦你的函数在多个操作码中访问一个变量,你的线程安全就消失了。如果将函数体包装在locks 中,则可以生成线程安全性。但请注意,锁的计算成本可能很高,并且可能会产生死锁。

回答by Rasmus Kaj

If you actually want to make your code notthread-safe, and have good chance of "bad" stuff actually happening without you trying like ten thousand times (or one time when you real don'twant "bad" stuff to happen), you can 'jitter' your code with explicit sleeps:

如果你真的想让你的代码不是线程安全的,并且有很大的机会在不尝试一万次的情况下真正发生“坏”的事情(或者一次你真的希望“坏”的事情发生),您可以使用显式睡眠“抖动”您的代码:

def íncrement():
    global c
    x = c
    from time import sleep
    sleep(0.1)
    c = x + 1

回答by ardsrk

Are you sure that the functions increment and decrement execute without any error?

您确定函数 increment 和 decrement 执行时没有任何错误吗?

I think it should raise an UnboundLocalError because you have to explicitly tell Python that you want to use the global variable named 'c'.

我认为它应该引发 UnboundLocalError,因为您必须明确告诉 Python 您要使用名为“c”的全局变量。

So change increment ( also decrement ) to the following:

因此,将 increment (也 decrement )更改为以下内容:

def increment():
    global c
    c += 1

I think as is your code is thread unsafe. This articleabout thread synchronisation mechanisms in Python may be helpful.

我认为你的代码是线程不安全的。这篇关于 Python 中线程同步机制的文章可能会有所帮助。