在Python中+=运算符是线程安全的吗?
我想写一段不安全的代码来做实验,这段代码会被两个线程调用。
c = 0
def increment():
c += 1
def decrement():
c -= 1
这段代码是线程安全的吗?
如果不是,能不能告诉我为什么它不安全,以及通常哪些情况会导致不安全的操作。
如果它是线程安全的,我该怎么做才能让它明确变得不安全呢?
8 个回答
(注意:你需要在每个函数中使用 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实现中,结果会完全不同。如果你需要线程安全,就需要使用锁机制。
不,这段代码绝对不安全,明显不适合多线程使用。
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
单个操作码是线程安全的,这要归功于全局解释器锁(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
每一个被多个线程共享的资源 都必须有一个锁。