1. 程式人生 > 實用技巧 >python多執行緒需要同步麼?

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是是釋放,
然後它倆只能等待下一次重啟了。

死鎖的症狀:

  1. 沒人幹活了,不佔cpu,程式卡死
  2. 程序沒有退出,佔著記憶體資源

當程式卡死時,可以看看是否還用cpu、如果用說不定過一會就不卡了,如果不用可能是死鎖了。