Python多線程中常見的lock

IO阻塞分析:

下面該需求很簡單將一個數值100做自減處到0.主函數中有0.1秒的IO阻塞

import threading
import time
def sub():
    global num   #  掌握爲什麼加global num
    temp=num
    time.sleep(0.1)
    num=temp-1

    time.sleep(2)
num=100
l=[]
for i in range(100):       
    t=threading.Thread(target=sub,args=())
    t.start()
    l.append(t)
for t in l:        #100個線程的IO阻塞
    t.join()
print(num)

"D:\Program Files\python.exe" E:/py_code/多線程/互斥鎖.py
99

Process finished with exit code 0

分析:
通過代碼我們可以看到因爲主函數中增加了一段IO阻塞的代碼,所以我們考慮用到了線程。
1.因爲Python GIL的特性原因,對於多線程並不能真正的實現並行而只能併發
2.當第一個線程搶到了GIL後(temp=100),其他的線程暫時不可以執行,但當第一個線程執行到time.sleep(0.1)時(IO阻塞),此時cup是閒置的,第二個線程可以搶GIL。

3.因爲第一個線程取到temp=100還沒有執行到num=temp-1所以此時第二個線程取到的值還是100,接着也執行到time.sleep(0.1),同理第三個線程搶到GIL。依次類推可以以知道這一百個線程在執行到time.sleep(0.1)之前得到的值都是100,當0.1秒過後全部都依次執行了num=temp-1,所以得出的結果是99.

所以很顯然上面的代碼沒辦法得到我們想要的結果,造成的原因就是IO阻塞和GIL相互作用的原因,所以下面會有另外的途徑去解決

互斥鎖


import threading
import time
def sub():
    global num   #  掌握爲什麼加global num
    lock.acquire()    #加鎖直到釋放鎖下個線程纔可以搶鎖
    temp=num
    time.sleep(0.1)
    num=temp-1
    lock.release()    #釋放鎖下個線程可以搶鎖了
    time.sleep(2)
num=100
l=[]
lock=threading.Lock()   #生成鎖對象
for i in range(100):       #生成100個線程
    t=threading.Thread(target=sub,args=())
    t.start()
    l.append(t)
for t in l:        #100個線程的IO阻塞
    t.join()
print(num)


"D:\Program Files\python.exe" E:/py_code/多線程/互斥鎖.py
0

Process finished with exit code 0

分析:

1.全局定義一把鎖,因爲進程處理的這個數據num是個公共數據,100個線程進行處理
2.主程序中處理數據的代碼只有下面的這三行:

temp=num
time.sleep(0.1)
num=temp-1

所以將這三行代碼鎖起來,使其在執行完之前其他的線程無法進行干預和取值執行。

3.所以將代碼前加鎖,數據做完運算後釋放鎖。當鎖釋放後其他線程就可以去搶佔這把鎖。

4.線程的運行流程是:
第一個線程搶到GIL,並拿到值temp=100,開始執行代碼,當執行到lock.acquire() 時,下面的代碼加鎖運行,一直執行到time.sleep(0.1),遇到IO阻塞,第二個線程開始搶GIL,搶到後執行代碼執行到lock.acquire(),此時該鎖已經被第一個線程搶佔還未釋放,所以第二個線程只能等第一個線程釋放。

5.當0.1s過後第一個線程繼續執行代碼num=temp-1,此時temp=99,接着執行到lock.release(),鎖釋放第二個線程搶到開始執行temp=num,而此時的temp=99,接着執行到time.sleep(0.1),第三個線程搶佔GIL,但因爲第二個線程未釋放定義鎖,所以無法繼續執行,此時第二個線程執行num=temp-1,此時的num=98.依此類推接下來的線程均依據進行運行。所以最後的執行結果等於0

遞歸鎖

死鎖:

所謂死鎖: 是指兩個或兩個以上的進程或線程在執行過程中,因爭奪資源而造成的一種互相等待的現象,若無外力作用,它們都將無法推進下去。此時稱系統處於死鎖狀態或系統產生了死鎖,這些永遠在互相等待的進程稱爲死鎖進程

import threading
import time
class MyThread(threading.Thread):
    def __init__(self):
        threading.Thread.__init__(self)
    def run(self):
        self.foo()
        self.bar()
    def foo(self):
        LockA.acquire()
        print("I am %s GET LOCKA---->%s"%(self.name,time.ctime()))
        LockB.acquire()
        print("I am %s GET LOCKB---->%s"%(self.name,time.ctime()))
        LockB.release()
        LockA.release()
    def bar(self):
        #time.sleep(2)
        LockB.acquire()
        print("I am %s GET LOCKB---->%s"%(self.name,time.ctime()))
        LockA.acquire()
        print("I am %s GET LOCKA---->%s"%(self.name,time.ctime()))
        LockA.release()
        LockB.release()
LockA=threading.Lock()
LockB=threading.Lock()
for i in range(10):
    t=MyThread()
    t.start()

"D:\Program Files\python.exe" E:/py_code/多線程/互斥鎖.py
I am Thread-1 GET LOCKA---->Tue Jul 18 18:46:27 2017
I am Thread-1 GET LOCKB---->Tue Jul 18 18:46:27 2017
I am Thread-1 GET LOCKB---->Tue Jul 18 18:46:27 2017
I am Thread-2 GET LOCKA---->Tue Jul 18 18:46:27 2017

分析:
1.上述的代碼定義了兩把鎖LockA和LockB,並定義一個類,類中繼承線程,則MyThread中就可以調用線程的所有方法。

2.在foo函數中在A鎖中嵌套了B鎖這這裏第一個線程進入執行的邏輯是,執行第一個函數foo,執行第一個打印然後啓動B鎖執行第二個打印釋放B鎖釋放A鎖進入到bar函數,於此同時第二個線程進入到要執行foo,當第一個線程釋放A鎖的那一刻,第二個線程馬上取到A鎖,但是此時第一個線程已經進入到bar函數也同一時刻取到B鎖執行第一個bar函數的第一個打印,當其執行到啓動A鎖時就會卡住,因爲此時的A鎖已經被第二個線程拿到,同理第二個線程此時在foo函數中想啓動B鎖,但是B鎖已經被第一個線程拿到,所以這個時候整個進程就會卡住

3.這是在互斥鎖中常見的問題,所以我們需要引進第二把鎖遞歸鎖:

遞歸鎖

在Python中爲了支持在同一線程中多次請求同一資源,python提供了可重入鎖RLock。這個RLock內部維護着一個Lock和一個counter變量,counter記錄了acquire的次數,從而使得資源可以被多次require。直到一個線程所有的acquire都被release,其他的線程才能獲得資源。上面的例子如果使用RLock代替Lock,則不會發生死鎖

import threading
import time
class MyThread(threading.Thread):
    def __init__(self):
        threading.Thread.__init__(self)
    def run(self):
        self.foo()
        self.bar()
    def foo(self):
        RLock.acquire()
        print("I am %s GET LOCKA---->%s"%(self.name,time.ctime()))
        RLock.acquire()
        print("I am %s GET LOCKB---->%s"%(self.name,time.ctime()))
        RLock.release()
        RLock.release()
    def bar(self):

        RLock.acquire()
        print("I am %s GET LOCKB---->%s"%(self.name,time.ctime()))
        RLock.acquire()
        print("I am %s GET LOCKA---->%s"%(self.name,time.ctime()))
        RLock.release()
        RLock.release()

RLock=threading.RLock()
for i in range(10):
    t=MyThread()
    t.start()



 "D:\Program Files\python.exe" E:/py_code/多線程/互斥鎖.py
I am Thread-1 GET LOCKA---->Tue Jul 18 19:19:17 2017
I am Thread-1 GET LOCKB---->Tue Jul 18 19:19:17 2017
I am Thread-1 GET LOCKB---->Tue Jul 18 19:19:17 2017
I am Thread-1 GET LOCKA---->Tue Jul 18 19:19:17 2017
I am Thread-2 GET LOCKA---->Tue Jul 18 19:19:17 2017
I am Thread-2 GET LOCKB---->Tue Jul 18 19:19:17 2017
I am Thread-2 GET LOCKB---->Tue Jul 18 19:19:17 2017
I am Thread-2 GET LOCKA---->Tue Jul 18 19:19:17 2017
I am Thread-4 GET LOCKA---->Tue Jul 18 19:19:17 2017
I am Thread-4 GET LOCKB---->Tue Jul 18 19:19:17 2017
I am Thread-4 GET LOCKB---->Tue Jul 18 19:19:17 2017
I am Thread-4 GET LOCKA---->Tue Jul 18 19:19:17 2017
I am Thread-5 GET LOCKA---->Tue Jul 18 19:19:17 2017
I am Thread-5 GET LOCKB---->Tue Jul 18 19:19:17 2017
I am Thread-5 GET LOCKB---->Tue Jul 18 19:19:17 2017
I am Thread-5 GET LOCKA---->Tue Jul 18 19:19:17 2017
I am Thread-6 GET LOCKA---->Tue Jul 18 19:19:17 2017
I am Thread-6 GET LOCKB---->Tue Jul 18 19:19:17 2017
I am Thread-6 GET LOCKB---->Tue Jul 18 19:19:17 2017
I am Thread-6 GET LOCKA---->Tue Jul 18 19:19:17 2017
I am Thread-3 GET LOCKA---->Tue Jul 18 19:19:17 2017
I am Thread-3 GET LOCKB---->Tue Jul 18 19:19:17 2017
I am Thread-3 GET LOCKB---->Tue Jul 18 19:19:17 2017
I am Thread-3 GET LOCKA---->Tue Jul 18 19:19:17 2017
I am Thread-8 GET LOCKA---->Tue Jul 18 19:19:17 2017
I am Thread-8 GET LOCKB---->Tue Jul 18 19:19:17 2017
I am Thread-8 GET LOCKB---->Tue Jul 18 19:19:17 2017
I am Thread-8 GET LOCKA---->Tue Jul 18 19:19:17 2017
I am Thread-9 GET LOCKA---->Tue Jul 18 19:19:17 2017
I am Thread-9 GET LOCKB---->Tue Jul 18 19:19:17 2017
I am Thread-9 GET LOCKB---->Tue Jul 18 19:19:17 2017
I am Thread-9 GET LOCKA---->Tue Jul 18 19:19:17 2017
I am Thread-7 GET LOCKA---->Tue Jul 18 19:19:17 2017
I am Thread-7 GET LOCKB---->Tue Jul 18 19:19:17 2017
I am Thread-7 GET LOCKB---->Tue Jul 18 19:19:17 2017
I am Thread-7 GET LOCKA---->Tue Jul 18 19:19:17 2017
I am Thread-10 GET LOCKA---->Tue Jul 18 19:19:17 2017
I am Thread-10 GET LOCKB---->Tue Jul 18 19:19:17 2017
I am Thread-10 GET LOCKB---->Tue Jul 18 19:19:17 2017
I am Thread-10 GET LOCKA---->Tue Jul 18 19:19:17 2017

Process finished with exit code 0

分析:

1.上述的代碼在全局定義了一把遞歸鎖

2.當第一個線程進入執行時先進入到foo函數,啓動第一把鎖並記錄一次接着執行第一個打印,然後啓動第二把鎖執行第二次打印並釋放兩把鎖。此時包括第一個線程在內這10個線程都能進行搶鎖,這裏RLock採取的就近原則,所以還是第一個線程搶到並進入到bar函數,此時其他線程是搶不到鎖只能繼續等待線程一釋放,線程一順序執行完bar函數並釋放RLock鎖,此刻線程一已經完整的執行完整個工作,並且線程2搶到鎖執行過程和線程一相同。

3.依次類推這10個線程會先後執行完各自的工作。如上述代碼執行的結果所示

信號量

Semaphore管理一個內置的計數器,
每當調用acquire()時內置計數器-1;
調用release() 時內置計數器+1;
計數器不能小於0;當計數器爲0時,acquire()將阻塞線程直到其他線程調用release()

import threading
import time
semaphore=threading.Semaphore(5)

def foo():
    semaphore.acquire()
    time.sleep(2)
    print("ok")
    semaphore.release()


for i in range(100):
    t=threading.Thread(target=foo,args=())
    t.start()
    

分析:

1.全局定義一個信號量,並限制最大接入的線程數爲5個

2.執行邏輯,當第一個線程進入後semaphore內置計數減1,則這裏可以接收4個線程,此時第一個線程打印第一個OK,接着第二個線程進入打印第二個OK,一直到第五個線程進入後,這時第六個線程在第五個線程沒有執行完semaphore.release()時是不可以進入的,所以按照這個邏輯,該端代碼的執行結果是每5個OK打印一次一共打印100個OK.

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章