【乾貨】趣味詳解 3 種 Python 執行緒鎖

cdfarsight發表於2021-12-10
今天本文將圍繞 threading 模組講解,基本上是純理論偏多。
對於日常開發者來講,這些內容是必備,同時也是高頻的面試常見問題。
官方文件( )


執行緒安全

執行緒安全是多執行緒或多程式程式設計中的一個概念,在擁有共享資料的多條執行緒並行執行的程式中,執行緒安全的程式碼會透過同步機制保證各個執行緒都可以正常且正確的執行,不會出現資料汙染等意外情況。
執行緒安全的問題最主要還是由執行緒切換導致的,比如一個房間(程式)中有10顆糖(資源),除此之外還有3個小人(1個主執行緒、2個子執行緒),當小人A吃了3顆糖後被系統強制進行休息時他認為還剩下7顆糖,而當小人B工作後又吃掉了3顆糖,那麼當小人A重新上崗時會認為糖還剩下7顆,但是實際上只有4顆了。
上述例子中執行緒A和執行緒B的資料不同步,這就是執行緒安全問題,它可能導致非常嚴重的意外情況發生,我們按下面這個示例來進行說明。
下面有一個數值num初始值為0,我們開啟2條執行緒:
  • 執行緒1對num進行一千萬次+1的操作
  • 執行緒2對num進行一千萬次-1的操作
結果可能會令人咋舌,num最後並不是我們所想象的結果0:

import threading

num = 0


def add():
    global num
    for i in range(10_000_000):
        num += 1


def sub():
    global num
    for i in range(10_000_000):
        num -= 1


if __name__ == "__main__":
    subThread01 = threading.Thread(target=add)
    subThread02 = threading.Thread(target=sub)

    subThread01.start()
    subThread02.start()

    subThread01.join()
    subThread02.join()

    print("num result : %s" % num)

# 結果三次採集
# num result : 669214
# num result : -1849179
# num result : -525674

上面這就是一個非常好的案例,想要解決這個問題就必須透過鎖來保障執行緒切換的時機。
需要我們值得留意的是,在Python基本資料型別中list、tuple、dict本身就是屬於執行緒安全的,所以如果有多個執行緒對這3種容器做操作時,我們不必考慮執行緒安全問題。

鎖的作用

鎖是Python提供給我們能夠自行操控執行緒切換的一種手段,使用鎖可以讓執行緒的切換變的有序。
一旦執行緒的切換變的有序後,各個執行緒之間對資料的訪問、修改就變的可控,所以若要保證執行緒安全,就必須使用鎖。
threading模組中提供了5種最常見的鎖,下面是按照功能進行劃分:
  • 同步鎖:lock(一次只能放行一個)
  • 遞迴鎖:rlock(一次只能放行一個)
  • 條件鎖:condi on(一次可以放行任意個)
  • 事件鎖:event(一次全部放行)
  • 訊號量鎖:semaphore(一次可以放行特定個)

1、Lock() 同步鎖 基本介紹

Lock鎖的稱呼有很多,如:
  • 同步鎖
  • 互斥鎖
它們是什麼意思呢?如下所示:
  • 互斥指的是某一資源同一時刻僅能有一個訪問者對其進行訪問,具有唯一性和排他性,但是互斥無法限制訪問者對資源的訪問順序,即訪問是無序的
  • 同步是指在互斥的基礎上(大多數情況),透過其他機制實現訪問者對資源的有序訪問
  • 同步其實已經實現了互斥,是互斥的一種更為複雜的實現,因為它在互斥的基礎上實現了有序訪問的特點
下面是threading模組與同步鎖提供的相關方法:

方法描述
threading.Lock()返回一個同步鎖物件
lockObject.acquire(blocking=True, timeout=1)上鎖,當一個執行緒在執行被上鎖程式碼塊時,將不允許切換到其他執行緒執行,預設鎖失效時間為1秒
lockObject.release()解鎖,當一個執行緒在執行未被上鎖程式碼塊時,將允許系統根據策略自行切換到其他執行緒中執行
lockObject.locaked()判斷該鎖物件是否處於上鎖狀態,返回一個布林值 使用方式

同步鎖一次只能放行一個執行緒,一個被加鎖的執行緒在執行時不會將執行權交出去,只有當該執行緒被解鎖時才會將執行權透過系統排程交由其他執行緒。
如下所示,使用同步鎖解決最上面的問題:

import threading

num = 0


def add():
    lock.acquire()
    global num
    for i in range(10_000_000):
        num += 1
    lock.release()


def sub():
    lock.acquire()
    global num
    for i in range(10_000_000):
        num -= 1
    lock.release()

if __name__ == "__main__":
    lock = threading.Lock()

    subThread01 = threading.Thread(target=add)
    subThread02 = threading.Thread(target=sub)

    subThread01.start()
    subThread02.start()

    subThread01.join()
    subThread02.join()

    print("num result : %s" % num)

# 結果三次採集
# num result : 0
# num result : 0
# num result : 0


這樣這個程式碼就完全變成了序列的狀態,對於這種計算密集型 業務來說,還不如直接使用序列化單執行緒執行來得快,所以這個例子僅作為一個示例,不能概述鎖真正的用途。

死鎖現象

對於同步鎖來說,一次acquire()必須對應一次release(),不能出現連續重複使用多次acquire()後再重複使用多次release()的操作,這樣會引起死鎖造成程式的阻塞,完全不動了,如下所示:

import threading

num = 0


def add():
    lock.acquire()  # 上鎖
    lock.acquire()  # 死鎖
    # 不執行
    global num
    for i in range(10_000_000):
        num += 1
    lock.release()
    lock.release()


def sub():
    lock.acquire()  # 上鎖
    lock.acquire()  # 死鎖
    # 不執行
    global num
    for i in range(10_000_000):
        num -= 1
    lock.release()
    lock.release()


if __name__ == "__main__":
    lock = threading.Lock()

    subThread01 = threading.Thread(target=add)
    subThread02 = threading.Thread(target=sub)

    subThread01.start()
    subThread02.start()

    subThread01.join()
    subThread02.join()

    print("num result : %s" % num)
with語句

由於threading.Lock()物件中實現了__enter__()與__exit__()方法,故我們可以使用with語句進行上下文管理形式的加鎖解鎖操作:

import threading

num = 0


def add():
    with lock:
        # 自動加鎖
        global num
        for i in range(10_000_000):
            num += 1
        # 自動解鎖


def sub():
    with lock:
        # 自動加鎖
        global num
        for i in range(10_000_000):
            num -= 1
        # 自動解鎖


if __name__ == "__main__":
    lock = threading.Lock()

    subThread01 = threading.Thread(target=add)
    subThread02 = threading.Thread(target=sub)

    subThread01.start()
    subThread02.start()

    subThread01.join()
    subThread02.join()

    print("num result : %s" % num)
   
# 結果三次採集
# num result : 0
# num result : 0
# num result : 0
2、RLock() 遞迴鎖 基本介紹

遞迴鎖是同步鎖的一個升級版本,在同步鎖的基礎上可以做到連續重複使用多次acquire()後再重複使用多次release()的操作,但是一定要注意加鎖次數和解鎖次數必須一致,否則也將引發死鎖現象。
下面是threading模組與遞迴鎖提供的相關方法:

方法描述
threading.RLock()返回一個遞迴鎖物件
lockObject.acquire(blocking=True, timeout=1)上鎖,當一個執行緒在執行被上鎖程式碼塊時,將不允許切換到其他執行緒執行,預設鎖失效時間為1秒
lockObject.release()解鎖,當一個執行緒在執行未被上鎖程式碼塊時,將允許系統根據策略自行切換到其他執行緒中執行
lockObject.locaked()判斷該鎖物件是否處於上鎖狀態,返回一個布林值 使用方式

以下是遞迴鎖的簡單使用,下面這段操作如果使用同步鎖則會發生死鎖現象,但是遞迴鎖不會:

import threading

num = 0


def add():
    lock.acquire()
    lock.acquire()
    global num
    for i in range(10_000_000):
        num += 1
    lock.release()
    lock.release()


def sub():
    lock.acquire()
    lock.acquire()
    global num
    for i in range(10_000_000):
        num -= 1
    lock.release()
    lock.release()


if __name__ == "__main__":
    lock = threading.RLock()

    subThread01 = threading.Thread(target=add)
    subThread02 = threading.Thread(target=sub)

    subThread01.start()
    subThread02.start()

    subThread01.join()
    subThread02.join()

    print("num result : %s" % num)

# 結果三次採集
# num result : 0
# num result : 0
# num result : 0
with語句

由於threading.RLock()物件中實現了__enter__()與__exit__()方法,故我們可以使用with語句進行上下文管理形式的加鎖解鎖操作:

import threading

num = 0


def add():
    with lock:
        # 自動加鎖
        global num
        for i in range(10_000_000):
            num += 1
        # 自動解鎖


def sub():
    with lock:
        # 自動加鎖
        global num
        for i in range(10_000_000):
            num -= 1
        # 自動解鎖


if __name__ == "__main__":
    lock = threading.RLock()

    subThread01 = threading.Thread(target=add)
    subThread02 = threading.Thread(target=sub)

    subThread01.start()
    subThread02.start()

    subThread01.join()
    subThread02.join()

    print("num result : %s" % num)

# 結果三次採集
# num result : 0
# num result : 0
# num result : 0
3、Condition() 條件鎖 基本介紹

條件鎖是在遞迴鎖的基礎上增加了能夠暫停執行緒執行的功能。並且我們可以使用wait()與notify()來控制執行緒執行的個數。
注意:條件鎖可以自由設定一次放行幾個執行緒。
下面是threading模組與條件鎖提供的相關方法:

方法描述
threading.Condition()返回一個條件鎖物件
lockObject.acquire(blocking=True, timeout=1)上鎖,當一個執行緒在執行被上鎖程式碼塊時,將不允許切換到其他執行緒執行,預設鎖失效時間為1秒
lockObject.release()解鎖,當一個執行緒在執行未被上鎖程式碼塊時,將允許系統根據策略自行切換到其他執行緒中執行
lockObject.wait(timeout=None)將當前執行緒設定為“等待”狀態,只有該執行緒接到“通知”或者超時時間到期之後才會繼續執行,在“等待”狀態下的執行緒將允許系統根據策略自行切換到其他執行緒中執行
lockObject.wait_for(predicate, timeout=None)將當前執行緒設定為“等待”狀態,只有該執行緒的predicate返回一個True或者超時時間到期之後才會繼續執行,在“等待”狀態下的執行緒將允許系統根據策略自行切換到其他執行緒中執行。注意:predicate引數應當傳入一個可呼叫物件,且返回結果為bool型別
lockObject.notify(n=1)通知一個當前狀態為“等待”的執行緒繼續執行,也可以透過引數n通知多個
lockObject.notify_all()通知所有當前狀態為“等待”的執行緒繼續執行 使用方式

下面這個案例會啟動10個子執行緒,並且會立即將10個子執行緒設定為等待狀態。
然後我們可以傳送一個或者多個通知,來恢復被等待的子執行緒繼續執行:

import threading

currentRunThreadNumber = 0
maxSubThreadNumber = 10


def task():
    global currentRunThreadNumber
    thName = threading.currentThread().name

    condLock.acquire()  # 上鎖
    print("start and wait run thread : %s" % thName)

    condLock.wait()  # 暫停執行緒執行、等待**
    currentRunThreadNumber += 1
    print("carry on run thread : %s" % thName)

    condLock.release()  # 解鎖


if __name__ == "__main__":
    condLock = threading.Condition()

    for i in range(maxSubThreadNumber):
        subThreadIns = threading.Thread(target=task)
        subThreadIns.start()

    while currentRunThreadNumber < maxSubThreadNumber:
        notifyNumber = int(
            input("Please enter the number of threads that need to be notified to run:"))

        condLock.acquire()
        condLock.notify(notifyNumber)  # 放行
        condLock.release()

    print("main thread run end")
   
# 先啟動10個子執行緒,然後這些子執行緒會全部變為等待狀態
# start and wait run thread : Thread-1
# start and wait run thread : Thread-2
# start and wait run thread : Thread-3
# start and wait run thread : Thread-4
# start and wait run thread : Thread-5
# start and wait run thread : Thread-6
# start and wait run thread : Thread-7
# start and wait run thread : Thread-8
# start and wait run thread : Thread-9
# start and wait run thread : Thread-10

# 批次傳送通知,放行特定數量的子執行緒繼續執行
# Please enter the number of threads that need to be notified to run:5  # 放行5個
# carry on run thread : Thread-4
# carry on run thread : Thread-3
# carry on run thread : Thread-1
# carry on run thread : Thread-2
# carry on run thread : Thread-5

# Please enter the number of threads that need to be notified to run:5  # 放行5個
# carry on run thread : Thread-8
# carry on run thread : Thread-10
# carry on run thread : Thread-6
# carry on run thread : Thread-9
# carry on run thread : Thread-7

# Please enter the number of threads that need to be notified to run:1
# main thread run end
with語句

由於threading.Condition()物件中實現了__enter__()與__exit__()方法,故我們可以使用with語句進行上下文管理形式的加鎖解鎖操作:

import threading

currentRunThreadNumber = 0
maxSubThreadNumber = 10


def task():
    global currentRunThreadNumber
    thName = threading.currentThread().name

    with condLock:
        print("start and wait run thread : %s" % thName)
        condLock.wait()  # 暫停執行緒執行、等待**
        currentRunThreadNumber += 1
        print("carry on run thread : %s" % thName)


if __name__ == "__main__":
    condLock = threading.Condition()

    for i in range(maxSubThreadNumber):
        subThreadIns = threading.Thread(target=task)
        subThreadIns.start()

    while currentRunThreadNumber < maxSubThreadNumber:
        notifyNumber = int(
            input("Please enter the number of threads that need to be notified to run:"))

        with condLock:
            condLock.notify(notifyNumber)  # 放行

    print("main thread run end")


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69996125/viewspace-2847006/,如需轉載,請註明出處,否則將追究法律責任。

相關文章