python多執行緒需要同步麼?
因為GIL的存在,每次只能執行一個執行緒,那Python還存在變數同步的問題麼?
宣告一個變數,起兩個執行緒各對這個變數加100,0000次,觀察結果是否為200,0000
預期:
如果不為200,0000,那說明Python的變數也需要同步。
程式碼:
import threading import time count = 0 def f(name): global count for i in range(1000000): count = count + 1 print(f"thread {name} end") threading.Thread(target=f, args=('t1',)).start() threading.Thread(target=f, args=('t2',)).start() time.sleep(1) print(f"sleep end, count is {count}")
輸出:
thread t1 end
thread t2 end
sleep end, count is 1465522
Python雖然沒有發揮出多核CPU的優勢,卻把執行緒不安全的問題帶來了,它在執行時也會編譯成
位元組碼,可以看下上面的程式碼翻譯成什麼了:
import dis
count = 0
def f(name):
global count
for i in range(1000000):
count = count + 1
print(dis.dis(f))
輸出:
9 0 SETUP_LOOP 24 (to 26) 2 LOAD_GLOBAL 0 (range) 4 LOAD_CONST 1 (1000000) 6 CALL_FUNCTION 1 8 GET_ITER >> 10 FOR_ITER 12 (to 24) # 跳轉到迭代運算 12 STORE_FAST 1 (i) 10 14 LOAD_GLOBAL 1 (count) # 讀取全域性變數 count 16 LOAD_CONST 2 (1) # 讀取常量 1 18 BINARY_ADD # 加運算 20 STORE_GLOBAL 1 (count) # 結果 count=1 回寫到count 22 JUMP_ABSOLUTE 10 >> 24 POP_BLOCK >> 26 LOAD_CONST 0 (None) 28 RETURN_VALUE None
想象一下兩個執行緒都執行這個方法,第一個執行到指令16或者18的時候第二個執行緒執行指令14
也就是他們進行加操作時讀取的是同一個count,比如都是8,他們的計算結果都是9,也就少加了一次。
運算的次數越多,出現上面的情況也就越多。
解決版本,把讀取count和+1操作合併成一個原子操作通過互斥鎖:
import threading from threading import Lock import time count = 0 lock = Lock() def f(name): global count global lock for i in range(1000000): lock.acquire() # 獲取鎖 count = count + 1 lock.release() # 釋放鎖 print(f"thread {name} end") threading.Thread(target=f, args=('t1',)).start() threading.Thread(target=f, args=('t2',)).start() time.sleep(1) print(f"sleep end, count is {count}")
這樣執行的結果就正常了,來再看下指令:
from threading import Lock
import dis
lock = Lock()
count = 0
def f(name):
global count
global lock
for i in range(1000000):
lock.acquire()
count = count + 1
lock.release()
print(dis.dis(f))
輸出:
/Users/wuhf/anaconda3/envs/cookdata/bin/python3 /Users/wuhf/PycharmProjects/cookdata/cookdata/tests/run_lock.py
10 0 SETUP_LOOP 40 (to 42)
2 LOAD_GLOBAL 0 (range)
4 LOAD_CONST 1 (1000000)
6 CALL_FUNCTION 1
8 GET_ITER
>> 10 FOR_ITER 28 (to 40)
12 STORE_FAST 1 (i)
11 14 LOAD_GLOBAL 1 (lock)
16 LOAD_METHOD 2 (acquire)
18 CALL_METHOD 0
20 POP_TOP
12 22 LOAD_GLOBAL 3 (count)
24 LOAD_CONST 2 (1)
26 BINARY_ADD
28 STORE_GLOBAL 3 (count)
13 30 LOAD_GLOBAL 1 (lock)
32 LOAD_METHOD 4 (release)
34 CALL_METHOD 0
36 POP_TOP
38 JUMP_ABSOLUTE 10
>> 40 POP_BLOCK
>> 42 LOAD_CONST 0 (None)
44 RETURN_VALUE
None
Process finished with exit code 0
關鍵指令分析:
執行到指令10進行迴圈迭代,進入迴圈體執行執行指令11,它載入並獲取鎖,接著指令12被合併成
一個大指令STORE_FAST
,這裡面讀取count,加操作並且回寫,指令13釋放鎖。
Lock雖然解決同步的問題,但是帶來的潛在的問題:死鎖!如果兩端段程式碼,兩個執行緒粉筆執行,
每一段都需要兩把鎖,並且都獲取了對方的鎖那會怎樣?
from threading import Lock
lock_a = Lock()
lock_b = Lock()
def f1():
global lock_a
global lock_b
lock_a.acquire()
lock_b.acquire()
print("Ha ha")
lock_a.release()
lock_b.release()
def f2():
global lock_a
global lock_b
lock_b.acquire()
lock_a.acquire()
print("He he")
lock_b.release()
lock_a.release()
想象一下,執行緒1執行f1,執行緒2執行f2,執行緒1執行剛執行完lock_a.acquire()
執行緒2也剛執行完
lock_b.acquire()
,這時候它倆手裡各有一把鎖,並且還需要一把鎖,執行緒1要執行lock_b.acquire()
,
但是這個已經被執行緒2持有了,要等待執行緒2釋放,執行緒2執行到lock_a.acquire()
等待執行緒1是是釋放,
然後它倆只能等待下一次重啟了。
死鎖的症狀:
- 沒人幹活了,不佔cpu,程式卡死
- 程序沒有退出,佔著記憶體資源
當程式卡死時,可以看看是否還用cpu、如果用說不定過一會就不卡了,如果不用可能是死鎖了。