1. 程式人生 > >Python GIL(Global Interpreter Lock)

Python GIL(Global Interpreter Lock)

一、介紹

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.)

結論:在 CPython 直譯器中,同一個程序下開啟的多執行緒,同一時刻只能有一個執行緒執行,無法利用多核優勢

  首先需要明確的一點是 GIL 並不是 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。

二、GIL 介紹

  GIL 本質就是一把互斥鎖,既然是互斥鎖,所有互斥鎖的本質都一樣,都是將併發執行變成序列,以此來控制同一時間內共享資料只能被一個任務所修改,進而保證資料安全。

  可以肯定的一點是:保護不同的資料的安全,就應該加不同的鎖。

  要想了解 GIL,首先確定一點:每次執行 python 程式,都會產生一個獨立的程序。例如 python test.py,python aaa.py,python bbb.py會產生 3 個不同的 python 程序

# test.py內容
import os, time
print(os.getpid())
time.sleep(
100) python3 test.py 會檢視到一個程序的pid 在 Windows 下 tasklist | findstr python 在 Linux 下 ps aux |grep python 會發現它們是相同的
驗證 python test.py 只會產生一個程序

  在一個 python 的程序內,不僅有 test.py 的主執行緒或者由該主執行緒開啟的其他執行緒,還有直譯器開啟的垃圾回收等直譯器級別的執行緒,總之,所有執行緒都執行在這一個程序內,毫無疑問:

  1、所有資料都是共享的,這其中,程式碼作為一種資料也是被所有執行緒共享的(test.py 的所有程式碼以及 CPython 直譯器的所有程式碼)

  例如:test.py 定義一個函式 work,在程序內所有執行緒都能訪問到 work 的程式碼,於是我們可以開啟三個執行緒然後 target 都指向該程式碼,能訪問到意味著就是可以執行。

  2、所有執行緒的任務,都需要將任務的程式碼當做引數傳給直譯器的程式碼去執行,即所有的執行緒要想執行自己的任務,首先需要解決的是能夠訪問到直譯器的程式碼。

 綜上:如果多個執行緒的 target=work,那麼執行流程是:

  多個執行緒先訪問到直譯器的程式碼,即拿到執行許可權,然後將 target 的程式碼交給直譯器的程式碼去執行

  直譯器的程式碼是所有執行緒共享的,所以垃圾回收執行緒也可能訪問到直譯器的程式碼而去執行,這就導致了一個問題:對於同一個資料 100,可能執行緒 1 執行 x=100 的同時,而垃圾回收執行的是回收 100 的操作,解決這種問題沒有什麼高明的方法,就是加鎖處理,如下圖的 GIL,保證 python 直譯器同一時間只能執行一個任務的程式碼

三、GIL 和 Lock

  Python 已經有一個 GIL 來保證同一時間只能有一個執行緒來執行了,為什麼這裡還需要 Lock?

  首先,我們需要達成共識:鎖的目的是為了保護共享的資料,同一時間只能有一個執行緒來修改共享的資料,然後,我們可以得出結論:保護不同的資料就應該加不同的鎖。最後,問題就很明朗了,GIL 與 Lock 是兩把鎖,保護的資料不一樣,前者是直譯器級別的(當然保護的就是直譯器級別的資料,比如垃圾回收的資料),後者是保護使用者自己開發的應用程式的資料,很明顯 GIL 不負責這件事,只能使用者自定義加鎖處理,即 Lock

  GIL 保護的是直譯器級別的資料,保護使用者自己的資料則需要自己加鎖處理,如下圖:

分析:

  1、100 個執行緒去搶 GIL 鎖,即搶執行許可權

  2、肯定有一個執行緒先搶到 GIL(暫且稱為執行緒 1),然後開始執行,一旦執行就會拿到 lock.acquire()

  3、極有可能執行緒 1 還未執行完畢,就有另外一個執行緒 2 搶到 GIL,然後開始執行,但執行緒 2 發現互斥鎖 Lock 還未被執行緒 1 釋放,於是阻塞,被迫交出執行許可權,即釋放 GIL

  4、直到執行緒 1 重新搶到 GIL,開始從上次暫停的位置繼續執行,直到正常釋放互斥鎖 Lock,然後其他的執行緒再重複 2 3 4 的過程

四、GIL 與多執行緒

  有了 GIL 的存在,同一程序內所有的執行緒在同一時刻只能有一個執行

  聽到這裡,有的人立馬質問:程序可以利用多核,但是開銷大,而 Python 的多執行緒開銷小,但卻無法利用多核優勢,也就是說 Python 沒用了?所以說,要解決這個問題,我們需要在幾個點上達成一致:

  1、CPU 到底是用來做計算的,還是用來做 I/O 的?

  2、多 CPU,意味著可以有多個核並行完成計算,所以多核提升的是計算效能

  3、每個 CPU 一旦遇到 I/O 阻塞,仍然需要等待,所以多核對 I/O 操作用處並不大

  一個工人相當於 CPU ,此時計算相當於工人在幹活,I/O 阻塞相當於為工人幹活提供所需原材料的過程,工人幹活的過程中如果沒有原材料了,則工人幹活的過程需要停止,直到等待原材料的到來。

  如果你的工廠乾的大多數任務都要有準備原材料的過程(I/O 密集型),那麼你有再多的工人,意義也不大,還不如一個人,在等材料的過程中讓工人去幹別的活,反過來講,如果你的工廠原材料都齊全,那當然是工人越多,效率越高

結論:

  對計算來說,CPU 越多越好,但是對於 I/O 來說,再多的 CPU 也沒用,當然對執行一個程式來說,隨著 CPU 的增多執行效率肯定會有所提高(不管提高幅度多大,總會有所提高),這是因為一個程式基本上不會是純計算或者純I/O,所以我們只能相對的去看一個程式到底是計算密集型還是 I/O 密集型,從而進一步分析 Python 的多執行緒到底有無用武之地

假設我們有四個任務需要處理,處理方式肯定是需要併發的效果,解決方案可以是:

  方案一:開啟四個程序

  方案二:一個程序下,開啟四個執行緒

單核情況下,分析結果:

  如果四個任務是計算密集型,沒有多核來平行計算,方案一徒增了建立程序的開銷。方案二更優。如果四個任務是 I/O 密集型,方案一建立程序的開銷大,且程序的切換速度遠不如執行緒。方案二更優

多核情況下,分析結果:

  如果四個任務是計算密集型,多核意味著平行計算,在 Python 中一個程序中同一時刻只有一個執行緒執行,比不上多核。方案一更優。如果四個任務是 I/O 密集型,再多的核也解決不了I/O問題,方案二更優。

結論:現在的計算機基本上都是多核,Python 對於計算密集型的任務開多執行緒的效率並不能帶來多大效能上的提升,甚至不如序列(沒有大量切換),但是,對於 I/O 密集型的任務效率還是有顯著提升的。