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

nub*_*ela 43 python increment thread-safety

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

c = 0

def increment():
  c += 1

def decrement():
  c -= 1
Run Code Online (Sandbox Code Playgroud)

这段代码线程安全吗?

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

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

Gle*_*ard 88

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

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
Run Code Online (Sandbox Code Playgroud)

一贯失败.

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

使其线程安全是直截了当的; 添加一个锁:

#!/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
Run Code Online (Sandbox Code Playgroud)

  • 比接受的答案更有帮助.谢谢! (10认同)
  • 上投了反对票.如果为每个增量获取并释放锁定而不是每100,000个增量,则您的锁定示例将更具说明性.如果他们要按顺序执行而没有任何重叠,为什么还要打扰线程呢? (4认同)

bob*_*nce 28

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

这段代码线程安全吗?

在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        
Run Code Online (Sandbox Code Playgroud)

因此,一个线程可以使用c和1加载到索引6,放弃GIL并让另一个线程执行inc并休眠,将GIL返回到第一个线程,该线程现在具有错误的值.

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


小智 15

确保我建议使用锁:

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
Run Code Online (Sandbox Code Playgroud)

synchronized装饰器还可以帮助保持代码易于阅读.


Joh*_*ooy 10

很容易证明您的代码不是线程安全的.你可以通过在关键部分使用睡眠来增加看到竞争状态的可能性(这只是模拟一个缓慢的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
Run Code Online (Sandbox Code Playgroud)

  • @omribahumi,什么?我觉得你对我答案的目的感到困惑.这段代码是一个_example_,它是多么容易_prove_特定的代码片段不是线程安全的.睡眠只是作为占位符来进行_simulate_通常存在的额外处理.如果你认为使用睡眠是避免竞争条件的错误方法,我当然同意,但这不是我的回答所声称的. (6认同)
  • @jacmkno,答案没有错,但由于某种原因使人困惑.它证明OP的代码是__not__线程安全的.或者你是否在暗示? (2认同)
  • 投票赞成这纯粹是因为您似乎因为其他人未读您的答案而受到了惩罚……对我来说很有意义 (2认同)

ebo*_*ebo 5

简短的回答:没有。

长答案:一般不会。

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

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


Joc*_*zel 0

由于 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
Run Code Online (Sandbox Code Playgroud)

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

  • 这并没有回答关于“+=”的问题 (9认同)