一、為什么有了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占有的資源,形成一個進程等待環路,環路中每一個進程所占有的資源同時被另一個申請,也就是前一個進程占有后一個進程所深情地資源。
只要發生死鎖,那么上面四個條件一定都成立。所以只要破壞其中一個,就可以打破死鎖。