聊聊Python中的GIL python中的GIL詳解

對於廣大寫Python的人來講,GIL(Global Interpreter Lock, 全局解釋器鎖)確定不陌生,但未必清楚GIL的歷史和全貌是怎樣的,今天咱們就來梳理一下GIL。html

1. 什麼是GILpython

GIL的全稱是 Global Interpreter Lock,全局解釋器鎖。之因此叫這個名字,是由於Python的執行依賴於解釋器。Python最初的設計理念在於,爲了解決多線程之間數據完整性和狀態同步的問題,設計爲在任意時刻只有一個線程在解釋器中運行。而當執行多線程程序時,由GIL來控制同一時刻只有一個線程可以運行。即Python中的多線程是表面多線程,也能夠理解爲fake多線程,不是真正的多線程。android

可能有的同窗會問,同一時刻只有一個線程可以運行,那麼是怎麼執行多線程程序的呢?其實原理很簡單:解釋器的分時複用。即多個線程的代碼,輪流被解釋器執行,只不過切換的很頻繁很快,給人一種多線程「同時」在執行的錯覺。聊的學術化一點,其實就是「併發」。程序員

再拓展一點「併發」和「並行」的概念:面試

普通解釋:
併發:交替作不一樣事情的能力
並行:同時作不一樣事情的能力
專業術語:
併發:不一樣的代碼塊交替執行
並行:不一樣的代碼塊同時執行編程

那麼問題來了,Python爲何要如此設計呢?即爲何要保證同一時刻只有一個線程在解釋器中運行呢安全

答案是爲了Python解釋器中原子操做的線程安全數據結構

 

2. 什麼是線程安全,什麼又是原子操做?多線程

2.1 線程安全併發

咱們首先要搞清楚什麼是進程,什麼是線程。進程是系統資源分配的最小單位,線程是程序執行的最小單位

舉一個例子,若是咱們把跑程序比做吃飯,那麼進程就是擺滿了飯菜的桌子,線程就是吃飯的那我的。

在多線程環境中,當各線程不共享數據的時候,那麼必定是線程安全的。問題是這種狀況並很少見,在多數狀況下須要共享數據,這時就須要進行適當的同步控制了。

線程安全通常都涉及到synchronized,就是多線程環境中,共享數據同一時間只能有一個線程來操做 否則中間過程可能會產生不可預製的結果

接着剛纔的例子,桌子上有三碗米飯,一我的正在吃,吃了兩碗米飯,可是尚未吃完,所以桌子上米飯的數量尚未更新;此時第二我的也想吃米飯,若是沒有線程安全方面的考慮,第二我的要是想直接拿三碗米飯吃,就會出錯。

2.2 原子操做

2.2.1 什麼是原子操做

原子操做就是不會由於進程併發或者線程併發而致使被中斷的操做原子操做的特色就是要麼一次所有執行,要麼全不執行。不存在執行了一半而被中斷的狀況。

當對全局資源存在寫操做時,若是不能保證寫入過程的原子性,會出現髒讀髒寫的狀況。

非原子操做示例:

import threading

count = 0

def run_thread():
    global count
    for i in range(10000):
        count += 1

t1 = threading.Thread(target=run_thread,args=())
t2 = threading.Thread(target=run_thread,args=())
t1.start()
t2.start()
t1.join()
t2.join()

print(count)

若是運行上面的代碼,打印出的count的結果是不肯定的,它會小於20000.但count實際是被增長了20000次。

爲何這樣呢?其實就是這裏的寫入操做count += 1並非原子的。它實際通過了三步:

1. 讀入count變量指向的值; 
2. +1
3. 讓count變量指向新的結果值。

原子操做示例:

lst = [4, 1, 3, 2]
 
def foo():
    lst.sort()

2.2.2 如何分辨原子操做與非原子操做

好比對下面這個函數:

n = 0
 
def foo():
    global n
    n += 1

咱們能夠看到這個函數用 Python 的標準 dis 模塊編譯的字節碼:

>>> import dis
>>> dis.dis(foo)
LOAD_GLOBAL              0 (n)
LOAD_CONST               1 (1)
INPLACE_ADD
STORE_GLOBAL             0 (n)

代碼的一行中, n += 1,被編譯成 4 個字節碼,進行 4 個基本操做:

  1. 將 n 值加載到堆棧上
  2. 將常數 1 加載到堆棧上
  3. 將堆棧頂部的兩個值相加
  4. 將總和存儲回 n
記住,一個線程每運行 1000 字節碼,就會被解釋器打斷奪走 GIL 。若是運氣很差,這(打斷)可能發生在線程加載 n 值到堆棧期間,以及把它存儲回 n 期間。很容易能夠看到這個過程會如何致使更新丟失:
threads = []
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,由於 100 個線程每一個都遞增 n 。但有時你會看到 99 或 98 ,若是一個線程的更新被另外一個覆蓋。
 
因此, 儘管有 GIL,你仍然須要加鎖來保護共享的可變狀態
n = 0
lock = threading.Lock()
 
def foo():
    global n
    with lock:
        n += 1

 

3. GIL的優勢與缺點

GIL的優勢是顯而易見的,GIL能夠保證咱們在多線程編程時,無需考慮多線程之間數據完整性和狀態同步的問題

GIL缺點是:咱們的多線程程序執行起來是「併發」,而不是「並行」。所以執行效率會很低,會不如單線程的執行效率。

網上不少人都提到過這樣的疑問:」爲何我多線程Python程序運行得比其只有一個線程的時候還要慢?「顯然,你們以爲一個具備兩個線程的程序要比其只有一個線程時要快。事實上,這個問題是確實存在的,緣由在於GIL的存在使得Python多線程程序的執行效率甚至比不上單線程的執行效率。很簡單,因爲GIL使得同一時刻只有一個線程在運行程序,再加上切換線程和競爭GIL帶來的開銷,顯然Python多線程的執行效率就比不上單線程的執行效率了。

 

4. 爲何會有GIL,GIL的歷史

你們顯然會繼續思考,爲何GIL須要保證只有一個線程在某一時刻處於運行中?難道不能夠添加細粒度的鎖來阻止多個獨立對象的同時訪問?而且爲何以前沒有人去嘗試過相似的事情?

這些實用的問題有着十分有趣的回答。首先要明確一點, Python解釋器的實現是有多個版本的:CPython, Jpython等。CPython就是用C語言實現Python解釋器,JPython是用Java實現Python解釋器。那麼 GIL的問題其實是存在於CPython中的。GIL的問題得不到解決,一方面是由於CPython中一開始就使用GIL的設計理念,而且不少Package依賴於CPython甚至依賴於GIL。所以形成尾大不掉,其實是個歷史問題。

爲了利用多核,Python開始支持多線程。而解決多線程之間數據完整性和狀態同步的最簡單方法天然就是加鎖。 因而有了GIL這把超級大鎖,而當愈來愈多的代碼庫開發者接受了這種設定後,他們開始大量依賴這種特性(即默認python內部對象是thread-safe的,無需在實現時考慮額外的內存鎖和同步操做)。

慢慢的這種實現方式被發現是蛋疼且低效的。但當你們試圖去拆分和去除GIL的時候,發現大量庫代碼開發者已經重度依賴GIL而很是難以去除了。有多難?作個類比,像MySQL這樣的「小項目」爲了把Buffer Pool Mutex這把大鎖拆分紅各個小鎖也花了從5.5到5.6再到5.7多個大版爲期近5年的時間,本且仍在繼續。MySQL這個背後有公司支持且有固定開發團隊的產品走的如此艱難,那又更況且Python這樣核心開發和代碼貢獻者高度社區化的團隊呢?

GIL對諸如當前線程狀態和爲垃圾回收而用的堆分配對象這樣的東西的訪問提供着保護。這是該實現的一種典型產物。如今也有其它的Python解釋器(和編譯器)並不使用GIL。雖然,對於CPython來講,自其出現以來已經有不少不使用GIL的解釋器。

那麼爲何不拋棄GIL呢?許多人也許不知道,在1999年,針對Python 1.5,一個常常被提到但卻不怎麼理解的「free threading」補丁已經嘗試實現了這個想法,該補丁來自Greg Stein。在這個補丁中,GIL被徹底的移除,且用細粒度的鎖來代替。然而,GIL的移除給單線程程序的執行速度帶來了必定的代價。當用單線程執行時,速度大約下降了40%。使用兩個線程展現出了在速度上的提升,但除了這個提升,這個收益並無隨着核數的增長而線性增加。因爲執行速度的下降,這一補丁被拒絕了,而且幾乎被人遺忘。

不過,「free threading」這個補丁是有啓發性意義的,其證實了一個關於Python解釋器的基本要點:移除GIL是很是困難的。因爲該補丁發佈時所處的年代,解釋器變得依賴更多的全局狀態,這使得想要移除當今的GIL變得更加困難。值得一提的是,也正是由於這個緣由,許多人對於嘗試移除GIL變得更加有興趣。困難的問題每每頗有趣。

可是這可能有點被誤導了。讓咱們考慮一下:若是咱們有了一個神奇的補丁,其移除了GIL,而且沒有對單線程的Python代碼產生性能上的降低,那麼咱們將會得到咱們一直想要的:一個線程API可能會同時利用全部的處理器。但這確實是一個好事嗎?

基於線程的編程毫無疑問是困難的。在編碼過程當中,老是會悄無聲息的出現一些新的問題。所以有一些很是知名的語言設計者和研究者已經總結得出了一些線程模型。就像某個寫過多線程應用的人能夠告訴你的同樣,無論是多線程應用的開發仍是調試都會比單線程的應用難上數倍。程序員一般所具備的順序執行的思惟模偏偏就是與並行執行模式不相匹配。GIL的出現無心中幫助了開發者免於陷入困境。在使用多線程時仍然須要同步的狀況下,GIL事實上幫助咱們保持不一樣線程之間的數據一致性問題。

因此簡單的說GIL的存在更多的是歷史緣由。若是推到重來,多線程的問題依然仍是要面對,可是至少會比目前GIL這種方式會更優雅。

 

5. 如何規避GIL帶來的影響

用multiprocess(多進程)替代Thread(推薦)

multiprocess庫的出現很大程度上是爲了彌補thread庫由於GIL而低效的缺陷。它完整的複製了一套thread所提供的接口方便遷移。惟一的不一樣就是它使用了多進程而不是多線程。每一個進程有本身的獨立的GIL,所以也不會出現進程之間的GIL爭搶

固然multiprocess也不是萬能良藥。它的引入會增長程序實現時線程間數據通信和同步的困難。就拿計數器來舉例子,若是咱們要多個線程累加同一個變量,對於thread來講,申明一個global變量,用thread.Lock的context包裹住三行就搞定了。而multiprocess因爲進程之間沒法看到對方的數據,只能經過在主線程申明一個Queue,put再get或者用share memory的方法。這個額外的實現成本使得原本就很是痛苦的多線程程序編碼,變得更加痛苦了。

用其餘解析器(不推薦)

以前也提到了既然GIL只是CPython的產物,那麼其餘解析器是否是更好呢?沒錯,像JPython和IronPython這樣的解析器因爲實現語言的特性,他們不須要GIL的幫助。然而因爲用了Java/C#用於解析器實現,他們也失去了利用社區衆多C語言模塊有用特性的機會。因此這些解析器也所以一直都比較小衆。畢竟功能和性能你們在初期都會選擇前者,Done is better than perfect。

GIL與互斥鎖

值得注意的是GIL 並不會保護開發者本身編寫的代碼。這是由於同一時刻當然只能有一個 Python 線程獲得執行,可是,當這個線程正在操做某個數據結構的時候,其餘線程可能會打斷它,一旦發生這種現象,就會破壞程序的狀態,從而使相關的數據結構沒法保持其一致性。爲了保證全部線程可以獲得公平地執行,Python 解釋器會給每一個線程分配大體相等的處理器時間。爲了達到這樣的分配策略,Python 系統可能當某個線程正在執行的時候將其暫停,而後使另外一個線程繼續往下執行。因爲咱們沒法提早獲知 Python 系統會在什麼時候暫停這些線程,因此咱們沒法控制程序中某些操做是原子操做。

爲了防止線程中出現數據競爭的行爲,使開發者能夠保護本身的數據結構不受破壞,Python 在 threading 模塊中提供了最簡單、最有用的工具:Lock 類,該類至關於互斥鎖。

在開發中咱們可使用互斥鎖來保護某個對象,使得在多線程同時訪問某個對象的時候,不會將該對象破壞。由於同一時刻,只有一個線程可以得到這把鎖。也就是說對將要訪問的對象進行隔離,那麼使用線程隔離的意義在於:是當前線程可以正確的引用到它本身創造的對象,而不是引用到其它線程鎖建立的對象。

 

總結

Python GIL實際上是功能和性能之間權衡後的產物,它尤爲存在的合理性,也有較難改變的客觀因素。咱們能夠作如下一些簡單的總結:

  • 由於GIL的存在,只有IO Bound場景下得多線程會獲得較好的性能
  • 若是對並行計算性能較高的程序能夠考慮把核心部分也成C模塊,或者索性用其餘語言實現
  • 在Python編程中,若是想利用計算機的多核提升程序執行效率,用多進程代替多線程
  • 即便有GIL存在,因爲GIL只保護Python解釋器的狀態,因此對於非原子操做,在Python進行多線程編程時也須要使用互斥鎖(如thread中的lock)保證線程安全。
  • GIL在較長一段時間內將會繼續存在,可是會不斷對其進行改進

 

參考連接:

1. 什麼是線程安全和線程不安全 https://blog.csdn.net/zjy_android_blog/article/details/69817476

2.  Python GIL https://www.aliyun.com/jiaocheng/446166.html

3.  python之理解GIL https://www.jianshu.com/p/573aaa001b35

4.  python面試不得不知道的點——GIL https://blog.csdn.net/weixin_41594007/article/details/79485847

5.  詳解Python GIL https://blog.csdn.net/liangkaiping0525/article/details/79490323

6.  深刻理解python多線程與GIL https://blog.csdn.net/ybdesire/article/details/77842438

7. python中的GIL詳解 https://www.cnblogs.com/SuKiWX/p/8804974.html

8.  深刻理解 GIL:如何寫出高性能及線程安全的 Python 代碼 http://python.jobbole.com/87743/

9.  談談有關 Python 的GIL 和 互斥鎖 https://blog.csdn.net/Amberdreams/article/details/81274217

10.  Python中的原子操做 https://www.jianshu.com/p/42060299c581

相關文章
相關標籤/搜索