我想爲實驗創建一個非線程安全的代碼塊,這些是2個線程將要調用的函數。Python中的+ =運算符是線程安全的嗎?
c = 0
def increment():
c += 1
def decrement():
c -= 1
此代碼是否線程安全?
如果不是,我可以理解它爲什麼不是線程安全的,以及哪種語句通常會導致非線程安全操作。
如果它是線程安全的,我怎樣才能使它顯式非線程安全?
我想爲實驗創建一個非線程安全的代碼塊,這些是2個線程將要調用的函數。Python中的+ =運算符是線程安全的嗎?
c = 0
def increment():
c += 1
def decrement():
c -= 1
此代碼是否線程安全?
如果不是,我可以理解它爲什麼不是線程安全的,以及哪種語句通常會導致非線程安全操作。
如果它是線程安全的,我怎樣才能使它顯式非線程安全?
單操作碼是線程安全的,因爲GIL,但沒有別的:
import time
class something(object):
def __init__(self,c):
self.c=c
def inc(self):
new = self.c+1
# if the thread is interrupted by another inc() call its result is wrong
time.sleep(0.001) # sleep makes the os continue another thread
self.c = new
x = something(0)
import threading
for _ in range(10000):
threading.Thread(target=x.inc).start()
print x.C# ~900 here, instead of 10000
每資源由多個線程共享必須有鎖。
這並不回答這個問題,這是關於'+ =' – 2016-12-08 14:53:09
另外,如果我錯了,請更正我,'print x.c'不等待線程完成。所以當你打印輸出時,它們大部分仍然在運行。 – 2016-12-14 09:56:07
簡答題:沒有。
長答案:一般不會。
雖然CPython的GIL生成單個操作碼thread-safe,但這不是普遍的行爲。你可能不會認爲像加法這樣簡單的操作就是原子指令。另一個線程運行時,添加可能只有一半。
只要您的函數在多個操作碼中訪問變量,您的線程安全性就會消失。如果將功能體包裝在locks中,則可以生成線程安全性。但請注意,鎖的計算成本可能很高,並可能產生死鎖。
(注:你需要global c
在每個功能使你的代碼工作。)
這段代碼線程安全的?
號只有一個字節碼指令在CPython的「原子」和+=
可能不會導致單個操作碼,即使所涉及的值是簡單的整數:
>>> c= 0
>>> def inc():
... global c
... c+= 1
>>> import dis
>>> dis.dis(inc)
3 0 LOAD_GLOBAL 0 (c)
3 LOAD_CONST 1 (1)
6 INPLACE_ADD
7 STORE_GLOBAL 0 (c)
10 LOAD_CONST 0 (None)
13 RETURN_VALUE
所以一個線程可以用c和1加載索引6,放棄GIL,並讓另一個線程進入,執行一個inc
並睡覺,返回GIL到第一個線程,該線程現在有錯誤的值。
在任何情況下,什麼是原子是您不應該依賴的實現細節。在未來的CPython版本中,字節碼可能會發生變化,並且在不依賴GIL的其他Python實現中結果將完全不同。如果您需要線程安全,則需要鎖定機制。
您確定函數遞增和遞減執行沒有任何錯誤?
我認爲它應該引發一個UnboundLocalError,因爲你必須明確告訴Python你想使用名爲'c'的全局變量。
因此改變增量(也遞減)以下:
def increment():
global c
c += 1
我覺得是你的代碼是線程不安全的。關於Python中的線程同步機制的This article可能會有所幫助。
如果你真的想使你的代碼不線程安全的,並有實際發生的「壞」的東西,而你試圖像一萬倍(或當你真正不想一次很好的機會, 「壞」的東西發生),你可以「抖動」你的代碼明確睡覺:
def íncrement():
global c
x = c
from time import sleep
sleep(0.1)
c = x + 1
很容易證明你的代碼是不是線程安全。您可以通過在關鍵部分使用睡眠來增加看到競爭條件的可能性(這只是模擬一個較慢的CPU)。但是,如果您足夠長時間運行代碼,最終不管最終都會看到競爭條件。
from time import sleep
c = 0
def increment():
global c
c_ = c
sleep(0.1)
c = c_ + 1
def decrement():
global c
c_ = c
sleep(0.1)
c = c_ - 1
使用睡眠這種東西是非常錯誤的。你是怎麼想出0.1的價值的?更快的處理器需要更長的睡眠時間嗎使用睡眠解決問題幾乎總是錯誤的。 – omribahumi 2015-03-26 19:52:24
@omribahumi,什麼?我認爲你對我答案的目的感到困惑。這段代碼是一個_example_,它證明了一段特定的代碼不是線程安全的。睡眠只是作爲一個佔位符來模擬額外的處理,通常在那裏。如果你的意思是說,使用睡眠是避免競爭條件的錯誤方式,我當然同意,但這不是我的答案。 – 2015-03-27 04:07:06
對不起,我的壞。我讀錯了「減少」。 – omribahumi 2015-03-27 16:38:57
不,這段代碼是絕對的,顯然不是線程安全的。
import threading
i = 0
def test():
global i
for x in range(100000):
i += 1
threads = [threading.Thread(target=test) for t in range(10)]
for t in threads:
t.start()
for t in threads:
t.join()
assert i == 1000000, i
不一致。
i + = 1解析爲四個操作碼:加載i,加載1,添加兩個,並將其存回i。 Python解釋器每100個操作碼切換活動線程(通過從一個線程釋放GIL,以便另一個線程可以擁有它)。 (這兩個都是實現細節。)當加載和存儲之間發生100-操作碼搶佔時,競爭條件就會發生,允許另一個線程開始遞增計數器。當它返回到掛起的線程時,它將繼續使用舊的「i」值,並取消其他線程在此期間運行的增量。
使線程安全性非常簡單;加鎖:
#!/usr/bin/python
import threading
i = 0
i_lock = threading.Lock()
def test():
global i
i_lock.acquire()
try:
for x in range(100000):
i += 1
finally:
i_lock.release()
threads = [threading.Thread(target=test) for t in range(10)]
for t in threads:
t.start()
for t in threads:
t.join()
assert i == 1000000, i
比接受的答案更有幫助。謝謝! – RoboCop87 2014-07-02 14:34:02
應該是被接受的答案。 – laike9m 2016-04-19 11:01:50
增加投票。如果獲取並釋放每個增量的鎖而不是每增加100,000個增量,則鎖示例將更具說明性。爲什麼即使打算按順序執行而沒有任何重疊,也會對線程感到困擾? – MarredCheese 2017-03-19 04:15:31
可以肯定的,我建議使用鎖:
import threading
class ThreadSafeCounter():
def __init__(self):
self.lock = threading.Lock()
self.counter=0
def increment(self):
with self.lock:
self.counter+=1
def decrement(self):
with self.lock:
self.counter-=1
同步的裝飾也可以幫助保持代碼易於閱讀。
在每個函數開始時應該有一個「全局c」減速,否則這並沒有真正做任何事情。 – JoshB 2016-09-01 08:04:34