2013-02-25 32 views
5

我的代码是为什么使用多线程获得总和是正确的?

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进行原子操作的?

+0

由于GIL我猜 – wim 2013-02-25 01:53:44

+0

特别是,CPython的确如此。 – Amber 2013-02-25 01:54:39

+0

@Mike什么操作是原子使用GIL时? – remykits 2013-02-25 01:59:12

回答

8

不要指望x += 1是线程安全的。这里是an example它不工作(见约西亚卡尔森的评论):

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

+0

好的。我忘记反汇编这些指令,以确保它们实际上是解释器中的单字节码调用。我删除了误导性评论。 – 2013-02-25 02:22:51

+0

最后,为了说明为什么这个问题没有出现在你的猜测中:解释者有一定数量的字节码操作,它在放弃GIL之前执行,这从我头顶开始是大约100。线程总是在这个限制内完成,因此看起来是原子的。 – lxop 2013-02-25 02:23:25

+0

@unutbu非常感谢你,它真的帮助我很多! – remykits 2013-02-25 03:34:42

相关问题