为什么使用多线程计算总和是正确的?

5 投票
1 回答
2160 浏览
提问于 2025-04-17 16:59

我的代码是

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 中花费了相当多的时间,这就增加了线程碰撞的机会。

撰写回答