为什么使用多线程计算总和是正确的?
我的代码是
import threading
counter = 0
def worker():
global counter
counter += 1
if __name__ == "__main__":
threads = []
for i in range(1000):
t = threading.Thread(target = worker)
threads.append(t)
t.start()
for t in threads:
t.join()
print counter
因为我没有使用锁来保护共享资源,也就是计数器变量,所以我本来希望结果会小于1000,但计数器的值总是1000,我不知道为什么。请问在Python中,counter += 1
是原子操作吗?
在Python中,哪些操作是原子操作,并且使用了全局解释器锁(GIL)?
1 个回答
8
不要指望 x += 1
是线程安全的。这里有一个例子说明它是如何不安全的(可以看看Josiah Carlson的评论):
import threading
x = 0
def foo():
global x
for i in xrange(1000000):
x += 1
threads = [threading.Thread(target=foo), threading.Thread(target=foo)]
for t in threads:
t.daemon = True
t.start()
for t in threads:
t.join()
print(x)
如果你拆解一下 foo
:
In [80]: import dis
In [81]: dis.dis(foo)
4 0 SETUP_LOOP 30 (to 33)
3 LOAD_GLOBAL 0 (xrange)
6 LOAD_CONST 1 (1000000)
9 CALL_FUNCTION 1
12 GET_ITER
>> 13 FOR_ITER 16 (to 32)
16 STORE_FAST 0 (i)
5 19 LOAD_GLOBAL 1 (x)
22 LOAD_CONST 2 (1)
25 INPLACE_ADD
26 STORE_GLOBAL 1 (x)
29 JUMP_ABSOLUTE 13
>> 32 POP_BLOCK
>> 33 LOAD_CONST 0 (None)
36 RETURN_VALUE
你会看到有一个 LOAD_GLOBAL
用来获取 x
的值,然后是一个 INPLACE_ADD
,最后是一个 STORE_GLOBAL
。
如果两个线程接连执行 LOAD_GLOBAL
,那么它们可能会都加载到 相同 的 x
值。然后它们都会把这个值加一,最后存储的结果也是相同的数字。这样一个线程的工作就覆盖了另一个线程的工作。这就不是线程安全的。
如你所见,如果程序是线程安全的,x
的最终值应该是2000000,但实际上你几乎总是得到一个小于2000000的数字。
如果你加上一个锁,你就能得到“预期”的结果:
import threading
lock = threading.Lock()
x = 0
def foo():
global x
for i in xrange(1000000):
with lock:
x += 1
threads = [threading.Thread(target=foo), threading.Thread(target=foo)]
for t in threads:
t.daemon = True
t.start()
for t in threads:
t.join()
print(x)
结果是
2000000
我认为你发布的代码没有问题的原因是:
for i in range(1000):
t = threading.Thread(target = worker)
threads.append(t)
t.start()
因为你的 worker
执行得非常快,相比于创建新线程所需的时间,实际上线程之间没有竞争。在上面的Josiah Carlson的例子中,每个线程在 foo
中花费了相当多的时间,这就增加了线程碰撞的机会。