python線程互斥鎖遞歸鎖死鎖


一、為什么有了GIL還要給線程加鎖

先說一下GIL,所謂的GIL,也叫全局解釋器鎖,它限制了任何時候都只能有一個線程進入CPU進行計算,所以python所謂的多線程並不能真正的並行。

那為什么有了GIL還需要給線程加鎖呢?不是直接一個線程處理完一個數據才輪到下一個線程進行嗎?線程鎖不是多此一舉?

解決這個問題,我們得更深入到底層看看代碼是怎么在CPU上運行的。在這里引入一個概念:原子操作

什么是原子操作

所謂的原子操作是指不會被線程調度機制打斷的操作;這種操作一旦開始,就一直運行到結束,不會運行到一半,然后CPU切換到另外的線程。原子操作可以是一個步驟,也可以是多個操作步驟,但是其順序不可以被打亂。

像 C語言的i++和python中的+=,-=,*=,/=都不是原子操作,他們在被翻譯成機器指令時實際上是分三個步驟的,比如 i-=1 這個操作本質是這樣的:

1、先把內存中的1存儲在CPU的寄存器中

2、CPU進行計算,減一

3、將寄存器的內容寫到內存中。

在1-3這個過程中,線程完全有可能被切換,所以可能導致線程數據的不安全。所以加鎖是必要的。我們看看下面的一個例子。

from threading import Lock,Thread
n = 10000000
def func():
    global n
    for i in range(1000000):
        n -= 1
t_lst = []
for i in range(10):
    t = Thread(target=func)
    t.start()
    t_lst.append(t)
for i in t_lst:i.join()
print(n)

 

上面代碼過程就是用十個線程去將一個數減到0,但是運行結果如下:

 

 所以這就驗證了線程數據的不安全性。下面是加鎖的版本

from threading import Lock,Thread
n = 10000000
def func(lock):
    global n
    for i in range(1000000):
        lock.acquire()
        n = n - 1
        lock.release()
t_lst = []
lock = Lock()
for i in range(10):
    t = Thread(target=func,args=(lock,))
    t.start()
    t_lst.append(t)
for i in t_lst:i.join()
print(n)

 

二、互斥鎖

同一時間只能有一個任務持有互斥鎖,而且只有這個任務可以對互斥鎖進行解鎖。當無法獲取鎖時,線程進入睡眠等待狀態。 

其實上面的例子用到的就是互斥鎖。當一個線程在操作數據n時候,其他線程是不允許對n進行操作的。

三、死鎖

所謂的死鎖就是指由多個線程直接,各自持有某些資源,又在申請其他線程所持有的資源,各自堅持着都不釋放資源,一直堅持着,這就是死鎖。

先不下明確的定義,后面再仔細討論。我們先來看看一個死鎖的例子。

科學家吃面問題:幾個科學家一起吃面,必須先申請面和申請到叉子才能開吃。

import time
from threading import Thread,Lock
def eat1(noodle_lock,fork_lock,name):
    noodle_lock.acquire()
    print(name,'拿到了面')
    fork_lock.acquire()
    print(name,'拿到了叉子')
    time.sleep(1)
    print(name,'吃到了面')
    fork_lock.release()
    noodle_lock.release()
    print(name, '放下了面')
    print(name, '放下了叉子')
def eat2(noodle_lock,fork_lock,name):
    fork_lock.acquire()
    print(name, '拿到了叉子')
    noodle_lock.acquire()
    print(name, '拿到了面')
    print(name, '吃到了面')
    noodle_lock.release()
    print(name, '放下了面')
    fork_lock.release()
    print(name, '放下了叉子')
name_list1 = ['特斯拉','牛頓']
name_list2 = ['法拉第','愛迪生']
noodle_lock  = Lock()
fork_lock = Lock()
for i in name_list1:
    t = Thread(target=eat1,args=(noodle_lock,fork_lock,i))
    t.start()
for i in name_list2:
    t = Thread(target=eat2, args=(noodle_lock, fork_lock, i))
    t.start()

一個拿着叉子在等面,一個拿着面在等叉子。一直僵持着,這就是死鎖。

四、遞歸鎖

 所謂的遞歸鎖就是指一個線程可以多次申請同一把鎖,但是不會造成死鎖。這就可以用來解決上面的死鎖問題

import time
from threading import Thread,RLock
def eat1(noodle_lock,fork_lock,name):
noodle_lock.acquire()
print(name,'拿到了面')
fork_lock.acquire()
print(name,'拿到了叉子')
time.sleep(1)
print(name,'吃到了面')
fork_lock.release()
noodle_lock.release()
print(name, '放下了面')
print(name, '放下了叉子')
def eat2(noodle_lock,fork_lock,name):
fork_lock.acquire()
print(name, '拿到了叉子')
noodle_lock.acquire()
print(name, '拿到了面')
print(name, '吃到了面')
noodle_lock.release()
print(name, '放下了面')
fork_lock.release()
print(name, '放下了叉子')
name_list1 = ['特斯拉','牛頓']
name_list2 = ['法拉第','愛迪生']
noodle_lock=fork_lock = RLock()
for i in name_list1:
t = Thread(target=eat1,args=(noodle_lock,fork_lock,i))
t.start()
for i in name_list2:
t = Thread(target=eat2, args=(noodle_lock, fork_lock, i))
t.start()

 

 

 

 

 下面在仔細討論一下死鎖。

五、死鎖產生的四個必要條件

1、互斥條件:當一個進程在訪問一個資源的時候,其他進程只能等待。即任何時候一個資源只能給一個進程使用。

2、不可剝奪條件:一個進程在訪問一個資源時,其他進程只能等該進程使用完釋放資源,不可強行剝奪。

3、請求和保持條件:當一個進程在申請它所需的資源時,並不會釋放已有的資源。

4、在發生死鎖時必然存在一個進程等待隊列{P1,P2,…,Pn},其中P1等待P2占有的資源,P2等待P3占有的資源,…,Pn等待P1占有的資源,形成一個進程等待環路,環路中每一個進程所占有的資源同時被另一個申請,也就是前一個進程占有后一個進程所深情地資源。 

只要發生死鎖,那么上面四個條件一定都成立。所以只要破壞其中一個,就可以打破死鎖。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM