Python 中的執行緒鎖

Muhammad Maisam Abbas 2023年1月30日 2022年5月18日
  1. Python 中的競爭條件
  2. Python 中的執行緒鎖
  3. 在 Python 中使用 with lock: 的執行緒鎖
Python 中的執行緒鎖

本教程將討論在 Python 中使用執行緒鎖的不同方法。

Python 中的競爭條件

競爭條件是當多個執行緒嘗試修改同一個共享變數時出現的問題。所有執行緒同時從共享變數中讀取相同的值。然後,所有執行緒嘗試修改共享變數的值。但是,該變數最終只會儲存最後一個執行緒的值,因為它會覆蓋前一個執行緒寫入的值。從這個意義上說,所有執行緒之間存在競爭,以檢視最終哪個執行緒修改了變數的值。以下程式碼中的示例演示了這種現象。

from threading import Thread

counter = 0

def increase(by):
    global counter
    local_counter = counter
    local_counter += by
    counter = local_counter
    print(f'counter={counter}')

t1 = Thread(target=increase, args=(10,))
t2 = Thread(target=increase, args=(20,))

t1.start()
t2.start()

t1.join()
t2.join()

print(f'The final counter is {counter}')

輸出:

counter=10
counter=20
The final counter is 20

我們有一個全域性共享變數 counter = 0 和兩個執行緒 t1t2。執行緒 t1 嘗試將 counter 的值增加 10,執行緒 t2 嘗試將 counter 的值增加 20。在上面的程式碼中,我們同時執行兩個執行緒並嘗試修改值計數器。根據上述邏輯,counter 的最終值應該是 30。但是,由於競爭條件,counter 要麼是 10,要麼是 20。

Python 中的執行緒鎖

執行緒鎖用於防止競爭條件。執行緒鎖在被一個執行緒使用時鎖定對共享變數的訪問,以便任何其他執行緒無法訪問它,然後線上程不使用共享變數時移除鎖,以便其他執行緒可以使用該變數進行處理。threading 模組中的 Lock class 用於在 Python 中建立執行緒鎖。acquire() 方法用於鎖定對共享變數的訪問,而 release() 方法用於解鎖鎖定。如果在未鎖定的鎖上使用 release() 方法會引發 RuntimeError 異常。

from threading import Thread, Lock

counter = 0

def increase(by, lock):
    global counter

    lock.acquire()

    local_counter = counter
    local_counter += by
    counter = local_counter
    print(f'counter={counter}')

    lock.release()

lock = Lock()

t1 = Thread(target=increase, args=(10, lock))
t2 = Thread(target=increase, args=(20, lock))

t1.start()
t2.start()

t1.join()
t2.join()

print(f'The final counter is {counter}')

輸出:

counter=10
counter=30
The final counter is 30

我們建立了一個全域性共享變數 counter=0 和兩個執行緒 t1t2。兩個執行緒都針對相同的 increase() 函式。increase(by, lock) 函式有兩個引數。第一個引數是它將增加 counter 的數量,第二個引數是 Lock 類的例項。除了前面的宣告,我們還在 Python 的 threading 模組中建立了一個 Lock 類的例項 lockincrease(by, lock) 函式中的這個 lock 引數使用 lock.acquire() 函式鎖定對 counter 變數的訪問,同時它被任何執行緒修改,並使用 lock 解鎖鎖定。release() 函式,當一個執行緒修改了 counter 變數。執行緒 t1counter 的值增加 10,執行緒 t2counter 的值增加 20。

由於執行緒鎖定,不會發生競爭條件,最終 counter 的值為 30。

在 Python 中使用 with lock: 的執行緒鎖

前一種方法的問題在於,當一個執行緒完成處理時,我們必須小心地解鎖每個鎖定的變數。如果沒有正確完成,我們的共享變數將只會被第一個執行緒訪問,而其他執行緒將無法訪問共享變數。這個問題可以通過使用上下文管理來避免。我們可以使用 with lock: 並將我們所有的關鍵程式碼放在這個塊中。這是防止競爭條件的更簡單的方法。以下程式碼片段顯示了使用 with lock: 來防止 Python 中的競爭條件。

from threading import Thread, Lock

counter = 0

def increase(by, lock):
    global counter

    with lock:
        local_counter = counter
        local_counter += by
        counter = local_counter
    print(f'counter={counter}')

lock = Lock()

t1 = Thread(target=increase, args=(10, lock))
t2 = Thread(target=increase, args=(20, lock))

t1.start()
t2.start()

t1.join()
t2.join()

print(f'The final counter is {counter}')

輸出:

counter=10
counter=30
The final counter is 30

我們將增加 counter 的程式碼放在 with lock: 塊中。執行緒 t1counter 的值增加 10,執行緒 t2counter 的值增加 20。沒有發生競爭條件,counter 的最終值為 30。此外,我們不需要擔心解鎖執行緒鎖。

兩種方法都完美地完成了它們的工作,即兩種方法都可以防止競爭條件的發生,但是第二種方法遠遠優於第一種,因為它可以避免我們為處理執行緒鎖的鎖定和解鎖而頭疼。與第一種方法相比,它編寫起來也更簡潔,更易於閱讀。

Muhammad Maisam Abbas avatar Muhammad Maisam Abbas avatar

Maisam is a highly skilled and motivated Data Scientist. He has over 4 years of experience with Python programming language. He loves solving complex problems and sharing his results on the internet.

LinkedIn

相關文章 - Python Thread