什么是python的全局解釋鎖(GIL)


GIL解決了Python中的什么問題?

為什么選取GIL作為解決方案?

對多線程Python程序的影響

為什么GIL還沒有被刪除?

為什么在Python 3 中GIL沒有被移除?

如何處理Python中的GIL?

我們所說的Python全局解釋鎖(GIL)簡單來說就是一個互斥體(或者說鎖),這樣的機制只允許一個線程來控制Python解釋器。

這就意味着在任何一個時間點只有一個線程處於執行狀態。GIL對執行單線程任務的程序員們來說並沒什么顯著影響,但是它成為了計算密集型(CPU-bound)和多線程任務的性能瓶頸。

由於GIL即使在擁有多個CPU核的多線程框架下都只允許一次運行一個線程,所以在Python眾多功能中其聲譽可謂是“臭名昭著”。

在這篇文章中,你將了解到GIL是如何影響到你的Python程序性能的以及如何減輕它對代碼帶來的影響。

GIL解決了Python中的什么問題?

Python利用引用計數來進行內存管理,這就意味着在Python中創建的對象都有一個引用計數變量來追蹤指向該對象的引用數量。當數量為0時,該對象占用的內存即被釋放。

我們來通過一個簡單的代碼演示引用計數是如何工作的:

在上述例子中,空列表對象[ ]的引用計數為3。該列表對象被a、b和傳遞給sys.getrefcount( )的參數引用。

回到GIL本身:

問題在於,這個引用計數變量需要在兩個線程同時增加或減少時從競爭條件中得到保護。如果發生了這種情況,可能會導致泄露的內存永遠不會被釋放,抑或更嚴重的是當一個對象的引用仍然存在的情況下錯誤地釋放內存。這可能會導致Python程序崩潰或帶來各種詭異的bug。

通過對跨線程分享的數據結構添加鎖定以至於數據不會不一致地被修改,這樣做可以很好的保證引用計數變量的安全。

但是對每一個對象或者對象組添加鎖意味着會存在多個鎖這也就導致了另外一個問題——死鎖(只有當存在多個鎖時才會發生)。而另一個副作用是由於重復獲取和釋放鎖而導致的性能下降。

GIL是解釋器本身的一個單一鎖,它增加的一條規則表明任何Python字節碼的執行都需要獲取解釋鎖。這有效地防止了死鎖(因為只存在一個鎖)並且不會帶來太多的性能開銷。但是這的確使每一個計算密集型任務變成了單線程。

GIL雖然也被其他語言解釋器使用(如Ruby),但是這不是解決這個問題的唯一辦法。一些編程語言通過使用除引用計數以外的方法(如垃圾收集)來避免GIL對線程安全內存管理的請求。

從另一方面來看,這也意味着這些語言通常需要添加其他性能提升功能(如JIT編譯器)來彌補GIL單線程性能優勢的損失。

為什么選取GIL作為解決方案?

那么為什么在Python中使用了這樣一種看似絆腳石的技術呢?這是Python開發人員的一個錯誤決定么?

正如Larry Hasting所說,GIL的設計決定是Python如今受到火熱追捧的重要原因之一。

當操作系統還沒有線程的概念的時候Python就一直存在着。Python設計的初衷是易於使用以便更快捷地開發,這也使得越來越多的程序員開始使用Python。

人們針對於C庫中那些被Python所需的功能寫了許多擴展,為了防止不一致變化,這些C擴展需要線程安全內存管理,而這些正是GIL所提供的。

GIL是非常容易實現而且很容易添加到Python中。因為只需要管理一個鎖所以對於單線程任務來說帶來了性能提升。

非線程安全的C庫變得更容易集成,而這些C擴展則成為Python被不同社區所接受的原因之一。

正如您所看到的,GIL是CPython開發者在早期Python生涯中面對困難問題的一種實用解決方案。

對多線程Python程序的影響

當你留意一些典型的Python程序或任何計算機程序時你會發現一個程序針對計算密集型和I/O密集型任務之間的性能表現是有所差異的。

計算密集型任務是那些促使CPU達到極限的任務。這其中包括了進行數學計算的程序,如矩陣相乘、搜索、圖像處理等。

I/O密集型任務是一些需要花費時間來等待來自用戶、文件、數據庫、網絡等的輸入輸出的任務。I/O密集型任務有時需要等待非常久直到他們從數據源獲取到他們所需要的內容為止。這是因為在准備好輸入輸出之前數據源本身需要先進行自身處理。舉例來說,一個用戶考慮在輸入提示中輸入什么或者在其自己進程中運行的數據庫查詢。

讓我們先來看一個執行倒計時的簡單的計算密集型程序:

在我的4核系統上運行得到以下輸出:

接下來我對代碼做出微調,使用兩個線程並行處理來完成倒計時:

接下來我再次運行:

正如你所看到的,兩個版本的完成時間相差無幾。在多線程版本中GIL阻止了計算密集型任務線程並行執行。

GIL對I/O密集型任務多線程程序的性能沒有太大的影響,因為在等待I/O時鎖可以在多線程之間共享。

但是對於一個線程是完全計算密集型的任務來說(例如,利用線程進行部分圖像處理)不僅會由於鎖而變成單線程任務而且還會明顯的增加執行時間。正如上例中多線程與完全單線程相比的結果。

這種執行時間的增加是由於鎖帶來的獲取和釋放開銷。

為什么GIL還沒有被刪除?

Python的開發者收到了許許多多關於這方面的抱怨,但是像Python這樣極受歡迎的語言無法做出去除GIL這樣的巨變同時還不造成向后不兼容問題。

GIL顯然是可以被刪除的,而且在過去這項任務也被開發者和研究人員多次完成。但是所有的嘗試打破了在很大程度上取決於由GIL提供解決方案的C擴展市場。

當然,還有許多其他解決方案可以解決GIL問題,但是其中一些以犧牲單線程和多線程I/O密集型任務的性能表現為代價,而另外一些解決方法又過於復雜。畢竟新版本發布后你不會希望你的Python跑得慢了些。

BDFL of Python的創始人Guido van Rossum在2007年09月的文章《It isn’t Easy to remove the GIL》中向社區做出回答:

“如果單線程任務和多線程I/O密集型任務的性能表現不會下降,那么我十分希望Py3k中能出現一組修補程序。”

當然了,此后的每一次嘗試都沒有滿足這個條件。

為什么在Python 3 中GIL沒有被移除?

Python3中的確有機會使得許多功能從零開始,並且在這個過程中打破了那些需要更改和更新的C擴展並且將其移植到Python 3中。這也是為什么Python 3的早期版本被社區采納的較慢的原因。

但是為什么GIL沒有被刪除?

刪除GIL會使得Python 3在處理單線程任務方面比Python 2慢,可以想像會產生什么結果。你不能否認GIL帶來的單線程性能優勢,這也就是為什么Python 3中仍然還有GIL。

但是Python 3的確對現有GIL做了重大改進。

我們僅僅討論了GIL對“僅計算密集型任務”和“僅I/O密集型任務”的影響,但是對於那些一部分線程是計算密集型一部分線程是I/O密集型的程序來說會怎么樣呢?

在這樣的程序中,Python的GIL通過不讓I/O密集型線程從計算密集型線程獲取GIL而使I/O密集型線程陷入癱瘓。

這是因為Python中內嵌了一種機制,這個機制在固定連續使用時間后強迫線程釋放GIL,並且如果沒人獲取這個GIL,那么同一線程可以繼續使用。

這個機制面臨的問題是大多數計算密集型線程會在別的線程獲取GIL之前再次獲取GIL。這個研究工作由David Beazley進行,並且你可以在這里得到可視化資源。

Antoine Pitrou於2009年在Python3.2中解決了這個問題,他添加了一種機制來查看其他線程請求GIL的訪問數量,當數量下降時不允許當前線程在其他線程有機會運行之前重新獲取GIL。

如何處理Python中的GIL?

如果GIL給你帶來困擾,你可嘗試一下方法:

多進程vs多線程:最流行的方法是應用多進程方法,在這個方法中你使用多個進程而不是多個線程。每一個Python進程都有自己的Python解釋器和內存空間,因此GIL不會成為問題。Python擁有一個multiprocessing模塊可以幫助我們輕松創建多進程:

在系統上運行得到

相比於多線程版本,性能有所提升。

但是時間並沒有下降到我們之前版本的一半,這是因為進程管理有自己的開銷。多進程比多線程更“重”,因此請記住,這可能成為規模瓶頸。

替代Python解釋器:Python中有多個解釋器實現辦法,分別用C,Java,C#和Python編寫的CPython,JPython,IronPython和PyPy是最受歡迎的。GIL只存在於傳統的Python實現方法如CPython中。如果你的程序及其庫文件可以通過別的實現方式實現,那么你也可以嘗試一下。

等等看吧:許多用戶利用GIL提升了單線程任務性能表現。當然多線程程序員們也不必為此煩惱,因為Python社區內的一些聰明大腦們正在致力於從CPython中刪除GIL。其中一種嘗試為Giletomy。

Python GIL經常被認為是一個神秘而困難的話題。但是請記住作為一名Python支持者,只有當您正在編寫C擴展或者您的程序中有計算密集型的多線程任務時才會被GIL影響。

在這種情況下,這篇文章應該給了你需要的一切去了解GIL是什么以及如何在自己的項目中處理它。如果您希望了解GIL的低層次內部運行,我建議您觀看David Beazley的Understanding the Python GIL。

 

 

Python的GIL是什么鬼,多線程性能究竟如何

 

前言:博主在剛接觸Python的時候時常聽到GIL這個詞,並且發現這個詞經常和Python無法高效的實現多線程划上等號。本着不光要知其然,還要知其所以然的研究態度,博主搜集了各方面的資料,花了一周內幾個小時的閑暇時間深入理解了下GIL,並歸納成此文,也希望讀者能通過次本文更好且客觀的理解GIL。

文章歡迎轉載,但轉載時請保留本段文字,並置於文章的頂部 作者:盧鈞軼(cenalulu) 本文原文地址:http://cenalulu.github.io/python/gil-in-python/

GIL是什么

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

那么CPython實現中的GIL又是什么呢?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.)

好吧,是不是看上去很糟糕?一個防止多線程並發執行機器碼的一個Mutex,乍一看就是個BUG般存在的全局鎖嘛!別急,我們下面慢慢的分析。


為什么會有GIL

由於物理上得限制,各CPU廠商在核心頻率上的比賽已經被多核所取代。為了更有效的利用多核處理器的性能,就出現了多線程的編程方式,而隨之帶來的就是線程間數據一致性和狀態同步的困難。即使在CPU內部的Cache也不例外,為了有效解決多份緩存之間的數據同步時各廠商花費了不少心思,也不可避免的帶來了一定的性能損失。

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

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

所以簡單的說GIL的存在更多的是歷史原因。如果推到重來,多線程的問題依然還是要面對,但是至少會比目前GIL這種方式會更優雅。

 

以上內容都來至於網絡

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM