python 深入瞭解GIL鎖詳細
前言:
python
的使用者都知道Cpython
解釋器有一個弊端,真正執行時同一時間隻會有一個線程執行,這是由於設計者當初設計的一個缺陷,裡面有個叫GIL鎖
的,但他到底是什麼?我們隻知道因為他導致python
使用多線程執行時,其實一直是單線程,但是原理卻不知道,那麼接下來我們就認識一下GIL鎖
1、什麼是GIL鎖
GIL
(Global Interpreter Lock)不是Python
獨有的特性,它隻是在實現CPython
(Python解釋器)時,引入的一個概念。
在官方網站中定義如下:
In CPython, the global interpreter lock, or GIL, is a mutex that prevents multiple native threads from executing Python bytecodes at once. This lock is necessary mainly because CPython’s memory management is not thread-safe. (However, since the GIL exists, other features have grown to depend on the guarantees that it enforces.)
由定義可知,GIL
是一個互斥鎖(mutex
)。它阻止瞭多個線程同時執行Python
字節碼,毫無疑問,這降低瞭執行效率。理解GIL的必要性,需要瞭解CPython
對於線程安全的內存管理機制。
2、CPython對線程安全的內存管理機制
Python
使用引用計數來進行內存管理,在Python
中創建的對象都會有引用計數,來記錄有多少個指針指向它。當引用計數的值為0時,就會自動釋放內存
我們來看一個小例子,來解釋引用計數的原理:
>>> import sys >>> a = [] >>> b = a >>> sys.getrefcount(a) 3
可以看到,a
的引用計數值為 3
,因為有 a
、b
和作為參數傳遞的 getrefcount
都引用瞭一個空列表。
如果有2個python
線程同時引用a
,那麼2個線程都會嘗試對其進行數據操作,多個線程同時對一個數據進行增加或減少的操作,如果發生這種情況,則可能導致內存泄漏
3、GIL鎖的產生
由於多個線程同時對數據進行操作,會引發數據不一致,導致內存泄漏,我們可以對其進行加鎖,所以Cpython
就創建瞭GIL
鎖
但是既然有瞭鎖,一個對象就需要一把鎖,那麼多個對象就會有多把鎖,可能會給我們帶來2個問題
- 1.死鎖(線程之間互相爭搶鎖的資源)
- 2.反復獲取和釋放鎖而導致性能降低。
為瞭保證單線程情況下python
的正常執行和效率,GIL鎖
(單一鎖)由此產生瞭,它添加瞭一個規則,即任何Python
字節碼的執行都需要獲取解釋器鎖。這樣可以防止死鎖(因為隻有一個鎖),並且不會帶來太多的性能開銷。但這實際上使所有受CPU
約束的Python
程序(指的是CPU
密集型程序)都是單線程的。
4、GIL鎖的底層原理
上面這張圖,就是 GIL
在 Python
程序的工作示例。其中,Thread 1
、2
、3
輪流執行,每一個線程在開始執行時,都會鎖住 GIL,以阻止別的線程執行;同樣的,每一個線程執行完一段後,會釋放 GIL,以允許別的線程開始利用資源。
線程釋放GIL鎖
有兩種情況:
- 一是遇到
IO操作
, - 二是
Time Tick
到期。IO操作
很好理解
比如發出一個http
請求,等待響應。那麼Time Tick到期是什麼呢?Time Tick
規定瞭線程的最長執行時間,超過時間後自動釋放GIL鎖。Python 3
以後,間隔時間大致為15毫秒
。
雖然都是釋放GIL鎖,但這兩種情況是不一樣的。比如,Thread1遇到IO操作釋放GIL,由Thread2和Thread3來競爭這個GIL鎖,Thread1不再參與這次競爭。如果是Thread1因為Time Tick到期釋放GIL(多數是CPU密集型任務),那麼三個線程可以同時競爭這把GIL鎖,可能出現Thread1
在競爭中勝出,再次執行的情況。單核CPU下,這種情況不算特別糟糕。因為隻有1個CPU,所以CPU的利用率是很高的。
在多核CPU下,由於GIL鎖的全局特性,無法發揮多核的特性,GIL鎖會使得多線程任務的效率大大降低。
Thread1在CPU1上運行,Thread2在CPU2上運行。GIL是全局的,CPU2上的Thread2需要等待CPU1上的Thread1讓出GIL鎖,才有可能執行。如果在多次競爭中,Thread1都勝出,Thread2沒有得到GIL鎖,意味著CPU2一直是閑置的,無法發揮多核的優勢。
為瞭避免同一線程霸占CPU,在python3.2
版本之後,線程會自動的調整自己的優先級,使得多線程任務執行效率更高。
既然GIL降低瞭多核的效率,那保留它的目的是什麼呢?這就和線程執行的安全有關。
5、Python GIL不能絕對保證線程安全
def add(): global n for i in range(10**1000): n = n +1 def sub(): global n for i in range(10**1000): n = n - 1 n = 0 import threading a = threading.Thread(target=add,) b = threading.Thread(target=sub,) a.start() b.start() a.join() b.join() print n
上面的程序對n做瞭同樣數量的加法和減法,那麼n理論上是0。但運行程序,打印n,發現它不是0。問題出在哪裡呢,問題在於python的每行代碼不是原子化的操作。比如n = n+1
這步,不是一次性執行的。如果去查看python編譯後的字節碼執行過程,可以看到如下結果。
19 LOAD_GLOBAL 1 (n) 22 LOAD_CONST 3 (1) 25 BINARY_ADD 26 STORE_GLOBAL 1 (n)
從過程可以看出,n = n +1
操作分成瞭四步完成。因此,n = n+1
不是一個原子化操作。
- 1.加載全局變量n
- 2.加載常數1
- 3.進行二進制加法運算
- 4.將運算結果存入變量n。
根據前面的線程釋放GIL鎖
原則,線程a執行這四步的過程中,有可能會讓出GIL
。如果這樣,n=n+1
的運算過程就被打亂瞭。最後的結果中,得到一個非零的n也就不足為奇。
6、總結
對於IO密集型應用,多線程的應用和多進程應用區別不大。即便有GIL
存在,由於IO操作
會導致GIL釋放,其他線程能夠獲得執行權限。由於多線程的通訊成本低於多進程,因此偏向使用多線程。
對於計算密集型應用,由於CPU一直處於被占用狀態,GIL鎖直到規定時間才會釋放,然後才會切換狀態,導致多線程處於絕對的劣勢,此時可以采用多進程+協程。
到此這篇關於python 深入瞭解GIL鎖詳細的文章就介紹到這瞭,更多相關python 深入瞭解GIL鎖內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!
參考資料:
https://realpython.com/python-gil/
https://zhuanlan.zhihu.com/p/97218985
推薦閱讀:
- Python解析器Cpython的GIL解釋器鎖工作機制
- 分析詳解python多線程與多進程區別
- Python多線程即相關理念詳解
- 對Python中GIL(全局解釋器鎖)的一點理解淺析
- 如何讓python程序正確高效地並發