为啥使用多线程得到总和是正确的?

Posted

技术标签:

【中文标题】为啥使用多线程得到总和是正确的?【英文标题】:why using multiple threading to get the sum is correct?为什么使用多线程得到总和是正确的? 【发布时间】:2013-02-25 01:46:23 【问题描述】:

我的代码是

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,我不知道为什么。 counter += 1 是 Python 中的原子操作吗?

Python 中的哪些操作是使用 GIL 的原子操作?

【问题讨论】:

我猜是因为 GIL 具体来说,CPython 可以。 @Mike 使用 GIL 时哪些操作是原子的? 【参考方案1】:

不要指望x += 1 是线程安全的。这是 an example 不起作用的地方(请参阅 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()

是因为你的workers 完成得如此之快,与产生一个新线程所需的时间相比,实际上线程之间没有竞争。在上面 Josiah Carlson 的示例中,每个线程都在 foo 中花费大量时间,这增加了线程冲突的机会。

【讨论】:

好收获。我忘记反汇编指令以确保它们实际上是解释器中的单字节码调用。我已经删除了误导性的 cmets。 在最后补充一下为什么这个问题没有出现的猜测:解释器在放弃 GIL 之前执行了一定数量的字节码操作,这让我不以为然大约是 100。所以工作线程总是会在这个限制内很好地完成,因此看起来是原子的。

以上是关于为啥使用多线程得到总和是正确的?的主要内容,如果未能解决你的问题,请参考以下文章

为啥我的多线程并行求和函数的向量受限于线程数?

对服务使用 AsyncTask 类仍然得到“跳过 31 帧!应用程序可能在其主线程上做太多工作。”。为啥?

为啥 JavaScript 不支持多线程?

串口传输用不用使用多线程 为啥

多线程并行计算数据总和 C语言demo

Java中的多线程矩阵乘法