Python並行程式設計(二):多執行緒鎖機制利用Lock與RLock實現執行緒同步

若數發表於2019-04-09

什麼是鎖機制?

要回答這個問題,我們需要知道為什麼需要使用鎖機制。前面我們談到一個程式內的多個執行緒的某些資源是共享的,這也是執行緒的一大優勢,但是也隨之帶來一個問題,即當兩個及兩個以上的執行緒同時訪問共享資源時,如果此時沒有預設對應的同步機制,就可能帶來同一時刻多個執行緒同時訪問同一個共享資源,即出現競態,多數情況下我們是不希望出現這樣的情況的,那麼怎麼避免呢?

Lock() 管理執行緒

先看一段程式碼:

import threading
import time
resource = 0

count = 1000000

resource_lock = threading.Lock()


def increment():
    global resource
    for i in range(count):
        resource += 1


def decerment():
    global resource
    for i in range(count):
        resource -= 1


increment_thread = threading.Thread(target=increment)
decerment_thread = threading.Thread(target=decerment)

increment_thread.start()
decerment_thread.start()

increment_thread.join()
decerment_thread.join()

print(resource)

複製程式碼

執行截圖如下:

執行結果
當我們多次執行時,可以看到最終的結果都幾乎不等於我們期待的值即resource初始值0

為什麼呢? 原因就是因為 += 和 -=並不是原子操作。可以使用dis模組檢視位元組碼:

import dis
def add(total):
    total += 1
def desc(total):
    total -= 1
total = 0
print(dis.dis(add))
print(dis.dis(desc))
# 執行結果:
#   3           0 LOAD_FAST                0 (total)
#               3 LOAD_CONST               1 (1)
#               6 INPLACE_ADD
#               7 STORE_FAST               0 (total)
#              10 LOAD_CONST               0 (None)
#              13 RETURN_VALUE
# None
#   5           0 LOAD_FAST                0 (total)
#               3 LOAD_CONST               1 (1)
#               6 INPLACE_SUBTRACT
#               7 STORE_FAST               0 (total)
#              10 LOAD_CONST               0 (None)
#              13 RETURN_VALUE
# None

複製程式碼

那麼如何保證初始值為0呢? 我們可以利用Lock(),程式碼如下:

import threading
import time
resource = 0

count = 1000000

resource_lock = threading.Lock()


def increment():
    global resource
    for i in range(count):
        resource_lock.acquire()
        resource += 1
        resource_lock.release()


def decerment():
    global resource
    for i in range(count):
        resource_lock.acquire()
        resource -= 1
        resource_lock.release()


increment_thread = threading.Thread(target=increment)
decerment_thread = threading.Thread(target=decerment)

increment_thread.start()
decerment_thread.start()

increment_thread.join()
decerment_thread.join()

print(resource)

複製程式碼

執行截圖如下:

執行結果
從執行結果可以看到,不論我們執行多少次改程式碼,其resource的值都為初始值0, 這就是Lock()的功勞,即它可以將某一時刻的訪問限定在單個執行緒或者單個型別的執行緒上,在訪問鎖定的共享資源時,必須要現獲取對應的鎖才能訪問,即要等待其他執行緒釋放資源,即resource_lock.release() 當然為了防止我們對某個資源鎖定後,忘記釋放鎖,導致死鎖,我們可以利用上下文管理器管理鎖實現同樣的效果:

import threading
import time
resource = 0

count = 1000000

resource_lock = threading.Lock()


def increment():
    global resource
    for i in range(count):
        with resource_lock:
                resource += 1


def decerment():
    global resource
    for i in range(count):
        with resource_lock:
                resource -= 1
        


increment_thread = threading.Thread(target=increment)
decerment_thread = threading.Thread(target=decerment)

increment_thread.start()
decerment_thread.start()
複製程式碼

RLock() 與Lock()的區別

我們需要知道Lock()作為一個基本的鎖物件,一次只能一個鎖定,其餘鎖請求,需等待鎖釋放後才能獲取,否則會發生死鎖:

import threading
resource.lock = threading.lock()

resource = 0

resource.lock.acquire()
resource.lock.acquire()
resource += 1
resource.lock.release()
resource.lock.release()
複製程式碼

為解決同一執行緒中不能多次請求同一資源的問題,python提供了“可重入鎖”:threading.RLockRLock內部維護著一個Lock和一個counter變數,counter記錄了acquire的次數,從而使得資源可以被多次acquire。直到一個執行緒所有的acquire都被release,其他的執行緒才能獲得資源 。用法和threading.Lock類相同,即比如遞迴鎖的使用:

import threading
lock = threading.RLock()
def dosomething(lock):
    lock.acquire()
    # do something
    lock.release()
    
lock.acquire()
dosomething(lock)
lock.release()
複製程式碼

相關文章