天天看點

Python基礎系列講解——線程鎖Lock的使用介紹

我們知道Python的線程是封裝了底層作業系統的線程,在Linux系統中是Pthread(全稱為POSIX Thread),在Windows中是Windows Thread。是以Python的線程是完全受作業系統的管理的。但是在計算密集型的任務中多線程反而比單線程更慢。

這是為什麼呢?

在CPython 解釋器中執行線程時,每一個線程開始執行時,都會鎖住 GIL,以阻止别的線程執行。同樣的,每一個線程執行完一段後,會釋放 GIL,以允許别的線程開始利用資源。畢竟,如果Python線程在開始的時候鎖住GIL而不去釋放GIL,那别的線程就沒有運作的機會了。

為什麼要這麼處理呢?

我們先來介紹下競争條件(race condition)這個概念。競争條件是指兩個或者多個線程同時競争通路的某個資源(該資源本身不能被同時通路),有可能因為時間上存在先後原因而出現問題,這種情況叫做競争條件(Race Condition)。(Python中程序是有獨立的資源配置設定,線程是共用資源配置設定)

回到CPython上,CPython是使用引用計數器來管理記憶體的,所有建立的對象,都會有一個引用計數來記錄有多少個指針指向它。如下所示:

a_val = []

def ReferCount():

print(sys.getrefcount(a_val)) # 2

b = a_val

c = a_val

print(sys.getrefcount(a_val)) # 4

當引用計數為0時,CPython解釋器會自動釋放記憶體。這樣一來,如果有兩個Python線程同時引用了一個變量,就會造成引用計數的競争條件(race condition)。是以引用計數變量需要在兩個線程同時增加或減少時從競争條件中得到保護。如果發生了這種情況,可能會導緻洩露的記憶體永遠不會被釋放,更嚴重的是當一個對象的引用仍然存在的情況下錯誤地釋放記憶體,導緻Python程式崩潰或帶來各種詭異的問題。

以下是官方給的解釋:

In CPython, the global interpreter lock, or GIL, is a mutex that prevents multiple native threads from executing Python bytecodes at once. This lock is necessary mainly because CPython’s memory management is not thread-safe. (However, since the GIL exists, other features have grown to depend on the guarantees that it enforces.)

如何繞過GIL的限制?

目前像NumPy的矩陣運算這些高性能的應用場景是通過C/C++來實作Python庫,可以避免CPython解釋器的GIL限制。另一方面,當涉及到對性能非常嚴格的應用場景時,可以把關鍵代碼用C/C++來實作,然後通過Python調用這些程式,以此擺脫GIL的限制。

有了GIL機制是否還需要考慮競争條件嗎?

GIL的設計是為了友善CPython解釋器層面的編寫者,而不是Python應用層面的程式員。作為Python的使用者,我們還是需要用Lock等工具來鎖住資源,來確定線程安全。

接下來我們就介紹下如何使用Lock機制。

Lock的使用主要有以下幾個方法:

mutex = threading.Lock() # 建立鎖

mutex.acquire([timeout]) # 鎖定

mutex.release() # 釋放

例如以下例程:

g_count = 0

def func(str_val):

global g_count

for i in range(1000000):

g_count += 1

print(str_val+':g_count=%s' % g_count)

def test_func_lock():

t1 = threading.Thread(target=func,args=['func1'])

t2 = threading.Thread(target=func,args=['func2'])

t1.start()

t2.start()

t1.join()

t2.join()

最終傳回的結果有這些情況:

func2:g_count=1509057 func1:g_count=1489782

func1:g_count=1305421 func2:g_count=1684556

func2:g_count=1545063 func1:g_count=1547995

……

理論上最後的結果應該是2000000,由于線程被調用執行的順序并不确定,同時存在執行遞增語句時切換線程,導緻最後的結果并不是正确結果。

我們通過建立一個線程鎖來解決這個問題。如下所示:

lock = threading.Lock()

lock.acquire()

lock.release()

執行結果為:func2:g_count=1988364 func1:g_count=2000000

比如線程t1使用lock.acquire()獲得了這個鎖,那麼線程t2就無法再獲得該鎖了,隻會阻塞在 lock.acquire()處,直到鎖被線程t1釋放,即執行lock.release()。如此一來就不會出現執行了一半就暫停去執行别的線程的情況,最後結果是正确的2000000。

最後給大家推薦一個更精簡的鎖的用法:

def threading_lock_test():

# 建立鎖

lock = threading.Lock()

# 使用鎖的老方法

try:

print('Critical section 1')

print('Critical section 2')

finally:

# 使用鎖的新方法

with lock:

Python基礎系列講解——線程鎖Lock的使用介紹

Python基礎系列講解——線程鎖Lock的使用介紹