線程同步被定義為一種機制,它確保兩個或多個並發線程不會同時執行某些稱為關鍵段的特定程序段。
關鍵部分是指訪問共享資源的程序部分。
例如,在下圖中,3 個線程嘗試同時訪問共享資源或關鍵部分。
對共享資源的並發訪問可能導致爭用情況。
當兩個或多個線程可以訪問共享數據並嘗試同時更改共享數據時,就會發生爭用情況。因此,變量的值可能是不可預測的,並且根據進程的上下文切換的時間而變化。
考慮下面的程序來理解爭用條件的概念:
import threading
# 全局變量 x
x = 0
def increment():
"""用於遞增全局變量x的函數"""
global x
x += 1
def thread_task():
""" 線程的任務調用增量函數100000次。"""
for _ in range(10000000):
increment()
def main_task():
global x
# 將全局變量x設置為0
x = 0
# 創建線程
t1 = threading.Thread(target=thread_task)
t2 = threading.Thread(target=thread_task)
# 開啟線程
t1.start()
t2.start()
# 等待線程完成
t1.join()
t2.join()
if __name__ == "__main__":
for i in range(10):
main_task()
print("迭代 {0}: x = {1}".format(i, x))
運行結果:
在上面的程序中:
x 的預期最終值為 200000,但我們在函數的 10 次迭代中得到main_task是一些不同的值。
(如果您每次的運行結果都一樣,可能是由於您的計算機性能比較好,可以嘗試這加大
thread_task()
方法的數據,如多加一個零或幾個零,如10000000次)
發生這種情況是由於線程對共享變量 x 的並發訪問。x 值的這種不可預測性只不過是競態條件。
下面給出的是一個圖表,顯示了在上面的程序中如何發生爭用條件:
請注意,上圖中 x 的預期值為 12,但由於爭用條件,結果是 11!
因此,我們需要一個工具來在多個線程之間進行適當的同步。
這裡我們就會用到線程鎖了
線程模塊提供了一個 Lock 類來處理爭用條件。鎖定是使用操作系統提供的信號量對象實現的。
信號量是一個同步對象,用於控制多個進程/線程對並行編程環境中公共資源的訪問。它只是操作系統(或內核)存儲中指定位置的值,每個進程/線程都可以檢查該值,然後進行更改。根據找到的值,進程/線程可以使用該資源,或者會發現它已在使用中,並且必須等待一段時間才能重試。信號量可以是二進制(0 或 1),也可以具有其他值。通常,使用信號量的進程/線程會檢查該值,然後,如果它使用資源,則更改該值以反映此值,以便後續信號量用戶將知道等待。
Lock 類提供以下方法:
獲取([阻塞]) : 獲取鎖。鎖可以是阻塞的,也可以是非阻塞的。
釋放() : 釋放鎖。
請考慮下面給出的示例:
import threading
# 全局變量 x
x = 0
def increment():
"""用於遞增全局變量x的函數"""
global x
x += 1
def thread_task(lock):
"""線程的任務調用增量函數100000次."""
for _ in range(100000):
lock.acquire()
increment()
lock.release()
def main_task():
global x
# 設置全局變量為 0
x = 0
# 創建線程鎖
lock = threading.Lock()
# 創建線程
t1 = threading.Thread(target=thread_task, args=(lock,))
t2 = threading.Thread(target=thread_task, args=(lock,))
# 開啟線程
t1.start()
t2.start()
# 等待所有線程完成
t1.join()
t2.join()
if __name__ == "__main__":
for i in range(10):
main_task()
print("迭代 {0}: x = {1}".format(i, x))
運行結果:
讓我們嘗試一步一步地理解上面的代碼:
首先,使用以下命令創建 Lock 對象:
lock = threading.Lock()
然後,將 lock 作為目標函數參數傳遞:
t1 = threading.Thread(target=thread_task, args=(lock,))
t2 = threading.Thread(target=thread_task, args=(lock,))
在目標函數的關鍵部分,我們使用 lock.acquire() 方法應用 lock。一旦獲得鎖,在使用 lock.release() 方法釋放鎖之前,沒有其他線程可以訪問關鍵部分(此處為增量函數)。
lock.acquire()
increment()
lock.release()
正如您在結果中看到的,x 的最終值每次都顯示為 200000(這是預期的最終結果)。
下面給出了一個圖表,描述了上述程序中鎖的實現:
最後,以下是多線程處理的一些優點和缺點:
優勢:
弊: