天天看點

python多線程中的死鎖與遞歸鎖

Python中的多線程是共享所在程序的資源和記憶體位址的,是以當多個線程同時操作同一個資料的時候,就容易出錯。如下

from threading import Thread,currentThread
import time
def task():
    global n
    tem = n
    time.sleep(0.01) # 在此處休息0.01秒,足夠開啟所有的線程,他們所擷取到的n都是0,這就導緻了最後輸出的n為1
    tem = tem + 1
    n = tem
    print("%s :n = %d" % (currentThread().name, n))

if __name__ == '__main__':
    n = 0
    th_li = []
    for i in range(10):
        th = Thread(target=task)
        th.start()
        th_li.append(th)
    for i in th_li:
        i.join()  # 等待所有線程執行完畢    
    print(n)
           

最後輸出n等于1。為了避免這種情況的發生,我們可以在程序start()後馬上join()這樣就能夠将并發改為串行。還有一種方式就是加鎖,和多程序中的互斥鎖一樣。

from threading import Thread,currentThread,Lock
import time

def task():
    metux.acquire()
    global n
    tem = n
    time.sleep(0.01)
    tem = tem + 1
    n = tem
    print("%s :n = %d" % (currentThread().name, n))
    metux.release()

if __name__ == '__main__':
    n = 0
    th_li = []
    metux = Lock()
    for i in range(10):
        th = Thread(target=task)
        th.start()
        th_li.append(th)
    for i in th_li:
        i.join()
    print(n)
           

我們可以通過加鎖和解鎖來将并發改為串行,保證了資料的安全。但如果我們加鎖之後,忘記了解鎖,這樣程式就會卡死。為此Python提供了一種便捷的寫法

def task():
    metux.acquire()
    global n
    tem = n
    time.sleep(0.01)
    tem = tem + 1
    n = tem
    print("%s :n = %d" % (currentThread().name, n))
    metux.release()
           

上面這段代碼可以簡寫為

def task():
    with metux:
        global n
        tem = n
        time.sleep(0.01)
        tem = tem + 1
        n = tem
        print("%s :n = %d" % (currentThread().name, n))
           

這樣就不用擔心加鎖和解鎖的問題。加鎖是為了将任務的某個部分由并發改為串行,進而保證資料的安全。我們有時候也需要給任務加多把鎖。但多把鎖也容易産生新的問題:死鎖

from threading import Thread,Lock
import time

class MyThread(Thread):

    def th1(self):
        l1.acquire()
        print(self.name," 拿到了 LOKC-1")
        l2.acquire()
        print(self.name, " 拿到了 LOKC-2")
        l2.release()
        l1.release()

    def th2(self):
        l2.acquire()
        print(self.name," 拿到了 LOKC-2")
        time.sleep(1)
        l1.acquire()
        print(self.name, " 拿到了 LOKC-1")
        l1.release()
        l2.release()

    def run(self):
        self.th1()
        self.th2()

if __name__ == "__main__":
    l1 = Lock()
    l2 = Lock()
    for i in range(10):
        mt = MyThread()
        mt.start()
           

執行結果

Thread-1  拿到了 LOKC-1
Thread-1  拿到了 LOKC-2
Thread-1  拿到了 LOKC-2
Thread-2  拿到了 LOKC-1
           

這裡Thread-1拿到了 LOKC-2,準備拿LOKC-1。這裡Thread-2拿到了 LOKC-1,準備拿LOKC-2。Thread-1需要的LOKC-1在Thread-2手中還沒有釋放掉,Thread-2需要的LOKC-2在Thread-1手中還沒有釋放掉。2個線程都在等待對方釋放自己所需要的鎖。程式就在此處卡死了。這就是死鎖現象。為了解決這個問題,Python中的遞歸鎖(RLock)就産生。

RLock内部包含着一個Lock和一個counter變量,counter記錄了acquire的次數,進而使得資源可以被多次acquire。直到一個線程所有的acquire都被release,其他的線程才能獲得資源。上面的例子如果使用RLock代替Lock,則不會發生死鎖: