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

65 投票
8 回答
23916 浏览
提问于 2025-04-15 15:54

我想写一段不安全的代码来做实验,这段代码会被两个线程调用。

c = 0

def increment():
  c += 1

def decrement():
  c -= 1

这段代码是线程安全的吗?

如果不是,能不能告诉我为什么它不安全,以及通常哪些情况会导致不安全的操作。

如果它是线程安全的,我该怎么做才能让它明确变得不安全呢?

8 个回答

33

(注意:你需要在每个函数中使用 global c 才能让你的代码正常工作。)

这段代码是线程安全的吗?

不是的。在CPython中,只有一条字节码指令是“原子”的,也就是说它是不可分割的。而 += 操作可能并不是一个单独的指令,即使参与运算的值很简单,比如整数:

>>> 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        

所以,一个线程可能会在加载了c和1后到达索引6,然后放弃全局解释器锁(GIL),让另一个线程进入。这个线程执行了一个 inc 操作并进入休眠状态,之后把GIL还给第一个线程,这样第一个线程就得到了错误的值。

总之,什么是原子操作是实现的细节,你不应该依赖它。字节码在未来的CPython版本中可能会改变,而在其他不依赖GIL的Python实现中,结果会完全不同。如果你需要线程安全,就需要使用锁机制。

133

不,这段代码绝对不安全,明显不适合多线程使用。

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

它总是会出错。

这里的“i += 1”其实是四个步骤:先读取i的值,再读取1,然后把这两个值加起来,最后把结果存回i。Python解释器每执行100个步骤就会切换线程(也就是把一个线程的控制权交给另一个线程)。这两个都是实现细节。问题出在,当这100个步骤的切换发生在读取和存储之间时,另一个线程可能会开始对计数器进行加1操作。当控制权回到被挂起的线程时,它会继续使用旧的“i”值,这样就会把其他线程的加1操作给抵消掉。

要让它变得线程安全其实很简单;只需要加一个锁:

#!/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
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

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

撰写回答