GIL鎖
計算機有4核,代表着同一時間,可以干4個任務。如果單核cpu的話,我啟動10個線程,我看上去也是並發的,因為是執行了上下文的切換,讓看上去是並發的。但是單核永遠肯定時串行的,它肯定是串行的,cpu真正執行的時候,因為一會執行1,一會執行2.。。。。正常的線程就是這個樣子的。但是,在python中,無論有多少核,永遠都是假象。無論是4核,8核,還是16核.......不好意思,同一時間執行的線程只有一個(線程),它就是這個樣子的。這個是python的一個開發時候,設計的一個缺陷,所以說python中的線程是假線程。
無論你啟多少個線程,你有多少個cpu, Python在執行的時候會淡定的在同一時刻只允許一個線程運行
2、GIL存在的意義?
因為python的線程是調用操作系統的原生線程,這個原生線程就是C語言寫的原生線程。因為python是用C寫的,啟動的時候就是調用的C語言的接口。因為啟動的C語言的遠程線程,那它要調這個線程去執行任務就必須知道上下文,所以python要去調C語言的接口的線程,必須要把這個上限問關系傳給python,那就變成了一個我在加減的時候要讓程序串行才能一次計算。就是先讓線程1,再讓線程2.......
每個線程在執行的過程中,python解釋器是控制不了的,因為是調的C語言的接口,超出了python的控制范圍,python的控制范圍是只在python解釋器這一層,所以python控制不了C接口,它只能等結果。所以它不能控制讓哪個線程先執行,因為是一塊調用的,只要一執行,就是等結果,這個時候4個線程獨自執行,所以結果就不一定正確了。有了GIL,就可以在同一時間只有一個線程能夠工作。雖然這4個線程都啟動了,但是同一時間我只能讓一個線程拿到這個數據。其他的幾個都干等。python啟動的4個線程確確實實落到了這4個cpu上,但是為了避免出錯。這也是Cpython的一個缺陷,其他語言沒有,僅僅只是Cpython有。
3、GIL鎖關系圖
GIL(全局解釋器鎖)是加在python解釋器里面的,效果如圖:

如上圖,為什么GIL鎖要加在python解釋器這一層,而卻不加在其他地方?
因為你python調用的所有線程都是原生線程。原生線程是通過C語言提供原生接口,相當於C語言的一個函數。你一調它,你就控制不了了它了,就必須等它給你返回結果。只要已通過python虛擬機,再往下就不受python控制了,就是C語言自己控制了。你加在python虛擬機以下,你是加不上去的。同一時間,只有一個線程穿過這個鎖去真正執行。其他的線程,只能在python虛擬機這邊等待。
總結:
需要明確的一點是GIL並不是Python的特性,它是在實現Python解析器(CPython)時所引入的一個概念。就好比C++是一套語言(語法)標准,但是可以用不同的編譯器來編譯成可執行代碼。有名的編譯器例如GCC,INTEL C++,Visual C++等。Python也一樣,同樣一段代碼可以通過CPython,PyPy,Psyco等不同的Python執行環境來執行。像其中的JPython就沒有GIL。然而因為CPython是大部分環境下默認的Python執行環境。所以在很多人的概念里CPython就是Python,也就想當然的把GIL歸結為Python語言的缺陷。所以這里要先明確一點:GIL並不是Python的特性,Python完全可以不依賴於GIL。
線程鎖(互斥鎖)
線程需要溝通,需要共享數據,但是我們之前並沒有涉及到多線程情況共享數據的例子。下面就來看看,多線程共享數據會出現什么情況
1、多進程共享數據(python2.7中實驗)
import threading
import time
def run(n):
global num # 把num變成全局變量
time.sleep(1) # 注意了sleep的時候是不占有cpu的,這個時候cpu直接把這個線程掛起了,此時cpu去干別的事情去了
num += 1 # 所有的線程都做+1操作
num = 0 # 初始化num為0
t_obj = list()
for i in range(100):
t = threading.Thread(target=run, args=("t-{0}".format(i),))
t.start()
t_obj.append(t)
for t in t_obj:
t.join()
print("--------all thread has finished")
print("num:", num) # 輸出最后的num值
#執行結果
--------all thead has finished
('num:', 97) #輸出的結果
最后輸出的結果怎么會是 97 呢?應該是100才對啊,不是有GIL(全局解釋器鎖)已經控制了,為什么最后的輸出結果還是錯誤?
其實這種情況只能在python2.x 中才會出現的,python3.x里面沒有這種現象,下面我們就用一張圖來解釋一下這個原因。如圖:

解釋:
- 到第5步的時候,可能這個時候python正好切換了一次GIL(據說python2.7中,每100條指令會切換一次GIL),執行的時間到了,被要求釋放GIL,這個時候thead 1的count=0並沒有得到執行,而是掛起狀態,count=0這個上下文關系被存到寄存器中.
- 然后到第6步,這個時候thead 2開始執行,然后就變成了count = 1,返回給count,這個時候count=1.
- 然后再回到thead 1,這個時候由於上下文關系,thead 1拿到的寄存器中的count = 0,經過計算,得到count = 1,經過第13步的操作就覆蓋了原來的count = 1的值,所以這個時候count依然是count = 1,所以這個數據並沒有保護起來。
2、添加線程鎖
通過上面的圖我們知道,結果依然是不准確的。所以我還要加一把鎖,這個是用戶級別的鎖。
import threading
import time
def run(n):
lock.acquire() # 添加線程鎖
global num # 把num變成全局變量
time.sleep(0.1) # 注意了sleep的時候是不占有cpu的,這個時候cpu直接把這個線程掛起了,此時cpu去干別的事情去了
num += 1 # 所有的線程都做+1操作
lock.release() # 釋放線程鎖
num = 0 # 初始化num為0
lock = threading.Lock() # 生成線程鎖實例
t_obj = list()
for i in range(10):
t = threading.Thread(target=run, args=("t-{0}".format(i),))
t.start()
t_obj.append(t)
for t in t_obj:
t.join() # 為join是等子線程執行的結果,如果不加,主線程執行完,下面就獲取不到子線程num的值了,共享數據num值就錯誤了
print("--------all thread has finished")
print("num:", num) # 輸出最后的num值
小結:
- 用theading.Lock()創建一個lock的實例。
- 在線程啟動之前通過lock.acquire()加加鎖,在線程結束之后通過lock.release()釋放鎖。
- 這層鎖是用戶開的鎖,就是我們用戶程序的鎖。跟我們這個GIL沒有關系,但是它把這個數據相當於copy了兩份,所以在這里加鎖,以確保同一時間只有一個線程,真真正正的修改這個數據,所以這里的鎖跟GIL沒有關系,你理解就是自己的鎖。
- 加鎖,說明此時我來去修改這個數據,其他人都不能動。然后修改完了,要把這把鎖釋放。這樣的話就把程序編程串行了。
3、使用場景
在用戶層面加鎖,使程序變成串行了,那我們在什么情況下用呢?
1、我們在程序中間不能有sleep,因為程序變成串行,這樣你再sleep,程序執行的時間就會變長。
2、我們使用的時候確保數據量不是特別大,如果數據量大,也會影響我們的執行效率。
3、如果你程序結束時,不釋放鎖的話,而且程序又是串行的,則就是占着坑,那永遠在那邊等着,所以最后需要釋放鎖。
遞歸鎖(RLock)
1、線程死鎖
在線程間共享多個資源的時候,如果兩個線程分別占有一部分資源並且同時等待對方的資源,就會造成死鎖,因為系統判斷這部分資源都
正在使用,所有這兩個線程在無外力作用下將一直等待下去
import threading
def run1():
print("grab the first part data")
lock.acquire() # 修改num前加鎖
global num
num += 1
lock.release() # 釋放鎖
return num
def run2():
print("grab the second part data")
lock.acquire() # 修改num2前加鎖
global num2
num2 += 1
lock.release() # 釋放鎖
return num2
def run3():
lock.acquire() # 加鎖
res = run1() # 執行run1函數
print('--------between run1 and run2-----')
res2 = run2() # 執行run2函數
lock.release() # 釋放鎖
print(res, res2)
if __name__ == '__main__':
num, num2 = 0, 0
lock = threading.Lock() # 設置鎖的全局變量
for i in range(10):
t = threading.Thread(target=run3)
t.start()
while threading.active_count() != 1: # 判斷是否只剩主線程了
print(threading.active_count())
else:
print('----all threads done---')
print(num, num2)
上面的執行結果,是無限的進入死循環,所以不能這么加,這個時候就需要用到遞歸鎖。
2、遞歸鎖(RLock)
import threading
def run1():
print("grab the first part data")
lock.acquire() # 修改num前加鎖
global num
num += 1
lock.release() # 釋放鎖
return num
def run2():
print("grab the second part data")
lock.acquire() # 修改num2前加鎖
global num2
num2 += 1
lock.release() # 釋放鎖
return num2
def run3():
lock.acquire() # 加鎖
res = run1() # 執行run1函數
print('--------between run1 and run2-----')
res2 = run2() # 執行run2函數
lock.release() # 釋放鎖
print(res, res2)
if __name__ == '__main__':
num, num2 = 0, 0
lock = threading.RLock() # 只用修改這里,把線程鎖lock()更改成遞歸鎖RLock()的全局變量
for i in range(10):
t = threading.Thread(target=run3)
t.start()
while threading.active_count() != 1: # 判斷是否只剩主線程了
print(threading.active_count())
else:
print('----all threads done---')
print(num, num2)
遞歸鎖原理其實很簡單:就是每開一把門,在字典里面存一份數據,退出的時候去到door1或者door2里面找到這個鑰匙退出,如圖:

lock = {
door1:key1,
door2:key2
}
注:遞歸鎖用於多重鎖的情況,如果只是一層鎖,就用不上遞歸鎖,在實際情況下,遞歸鎖場景用的不是特別多。
