解释为脑瘫的那张图_Python GIL全局解释器锁详解(深度剖析)
通過前面的學習,我們了解了 Pyton 并發編程的特性以及什么是多線程編程。其實除此之外,Python 多線程還有一個很重要的知識點,就是本節要講的 GIL。GIL,中文譯為全局解釋器鎖。在講解 GIL 之前,首先通過一個例子來直觀感受一下 GIL 在 Python 多線程程序運行的影響。首先運行如下程序:
import timestart = time.clock()def CountDown(n): while n > 0: n -= 1CountDown(100000)print("Time used:",(time.clock() - start))運行結果為:
Time used: 0.0039529000000000005
在我們的印象中,使用多個(適量)線程是可以加快程序運行效率的,因此可以嘗試將上面程序改成如下方式:
import timefrom threading import Threadstart = time.clock()def CountDown(n): while n > 0: n -= 1t1 = Thread(target=CountDown, args=[100000 // 2])t2 = Thread(target=CountDown, args=[100000 // 2])t1.start()t2.start()t1.join()t2.join()print("Time used:",(time.clock() - start))運行結果為:
Time used: 0.006673
可以看到,此程序中使用了 2 個線程來執行和上面代碼相同的工作,但從輸出結果中可以看到,運行效率非但沒有提高,反而降低了。
如果使用更多線程進行嘗試,會發現其運行效率和 2 個線程效率幾乎一樣(本機器測試使用 4 個線程,其執行效率約為 0.005)。這里不再給出具體測試代碼,有興趣的讀者可自行測試。
是不是和你猜想的結果不一樣?事實上,得到這樣的結果是肯定的,因為 GIL 限制了 Python 多線程的性能不會像我們預期的那樣。那么,什么是 GIL 呢?GIL 是最流程的 CPython 解釋器(平常稱為 Python)中的一個技術術語,中文譯為全局解釋器鎖,其本質上類似操作系統的 Mutex。GIL 的功能是:在 CPython 解釋器中執行的每一個 Python 線程,都會先鎖住自己,以阻止別的線程執行。當然,CPython 不可能容忍一個線程一直獨占解釋器,它會輪流執行 Python 線程。這樣一來,用戶看到的就是“偽”并行,即 Python 線程在交替執行,來模擬真正并行的線程。有讀者可能會問,既然 CPython 能控制線程偽并行,為什么還需要 GIL 呢?其實,這和 CPython 的底層內存管理有關。CPython 使用引用計數來管理內容,所有 Python 腳本中創建的實例,都會配備一個引用計數,來記錄有多少個指針來指向它。當實例的引用計數的值為 0 時,會自動釋放其所占的內存。舉個例子,看如下代碼:
>>> import sys
>>> a = []
>>> b = a
>>> sys.getrefcount(a)
3
可以看到,a 的引用計數值為 3,因為有 a、b 和作為參數傳遞的 getrefcount 都引用了一個空列表。假設有兩個 Python 線程同時引用 a,那么雙方就都會嘗試操作該數據,很有可能造成引用計數的條件競爭,導致引用計數只增加 1(實際應增加 2),這造成的后果是,當第一個線程結束時,會把引用計數減少 1,此時可能已經達到釋放內存的條件(引用計數為 0),當第 2 個線程再次視圖訪問 a 時,就無法找到有效的內存了。所以,CPython 引進 GIL,可以最大程度上規避類似內存管理這樣復雜的競爭風險問題。
Python GIL底層實現原理
圖 1 GIL 工作流程示意圖
上面這張圖,就是 GIL 在 Python 程序的工作示例。其中,Thread 1、2、3 輪流執行,每一個線程在開始執行時,都會鎖住 GIL,以阻止別的線程執行;同樣的,每一個線程執行完一段后,會釋放 GIL,以允許別的線程開始利用資源。讀者可能會問,為什么 Python 線程會去主動釋放 GIL 呢?畢竟,如果僅僅要求 Python 線程在開始執行時鎖住 GIL,且永遠不去釋放 GIL,那別的線程就都沒有運行的機會。其實,CPython 中還有另一個機制,叫做間隔式檢查(check_interval),意思是 CPython 解釋器會去輪詢檢查線程 GIL 的鎖住情況,每隔一段時間,Python 解釋器就會強制當前線程去釋放 GIL,這樣別的線程才能有執行的機會。注意,不同版本的 Python,其間隔式檢查的實現方式并不一樣。早期的 Python 是 100 個刻度(大致對應了 1000 個字節碼);而 Python 3 以后,間隔時間大致為 15 毫秒。當然,我們不必細究具體多久會強制釋放 GIL,讀者只需要明白,CPython 解釋器會在一個“合理”的時間范圍內釋放 GIL 就可以了。整體來說,每一個 Python 線程都是類似這樣循環的封裝,來看下面這段代碼:
for (;;) { if (--ticker < 0) { ticker = check_interval; /* Give another thread a chance */ PyThread_release_lock(interpreter_lock); /* Other threads may run now */ PyThread_acquire_lock(interpreter_lock, 1); } bytecode = *next_instr++; switch (bytecode) { /* execute the next instruction ... */ }}從這段代碼中可以看出,每個 Python 線程都會先檢查 ticker 計數。只有在 ticker 大于 0 的情況下,線程才會去執行自己的代碼。
Python GIL不能絕對保證線程安全
注意,有了 GIL,并不意味著 Python 程序員就不用去考慮線程安全了,因為即便 GIL 僅允許一個 Python 線程執行,但別忘了 Python 還有 check interval 這樣的搶占機制。比如,運行如下代碼:
import threadingn = 0def foo(): global n n += 1threads = []for i in range(100): t = threading.Thread(target=foo) threads.append(t)for t in threads: t.start()for t in threads: t.join()print(n)執行此代碼會發現,其大部分時候會打印 100,但有時也會打印 99 或者 98,原因在于 n+=1 這一句代碼讓線程并不安全。如果去翻譯 foo 這個函數的字節碼就會發現,它實際上是由下面四行字節碼組成:
>>> import dis
>>> dis.dis(foo)
LOAD_GLOBAL????????????? 0 (n)
LOAD_CONST?????????????? 1 (1)
INPLACE_ADD
STORE_GLOBAL???????????? 0 (n)
而這四行字節碼中間都是有可能被打斷的!所以,千萬別以為有了 GIL 程序就不會產生線程問題,我們仍然需要注意線程安全。
總結
以上是生活随笔為你收集整理的解释为脑瘫的那张图_Python GIL全局解释器锁详解(深度剖析)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: linux 查看tongweb进程,To
- 下一篇: java c++ python哪个好_程