2013-02-25 71 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

相關問題