發(fā)布于:2021-01-21 09:08:26
0
313
0
簡(jiǎn)單地說,Python全局解釋器鎖或GIL是一個(gè)互斥鎖(或鎖),它只允許一個(gè)線程持有Python解釋器的控制權(quán)。
這意味著在任何時(shí)間點(diǎn),只有一個(gè)線程可以處于執(zhí)行狀態(tài)。GIL的影響對(duì)于執(zhí)行單線程程序的開發(fā)人員來說是看不到的,但是它可能成為CPU受限和多線程代碼的性能瓶頸。
由于GIL一次只允許執(zhí)行一個(gè)線程,即使在具有多個(gè)CPU核的多線程體系結(jié)構(gòu)中也是如此,因此GIL被譽(yù)為Python的“臭名昭著”特性。
在本文中,您將了解GIL如何影響Python程序的性能,以及如何減輕它對(duì)代碼的影響。
GIL為Python解決了什么問題?
Python使用引用計(jì)數(shù)進(jìn)行內(nèi)存管理。這意味著在Python中創(chuàng)建的對(duì)象有一個(gè)reference count變量,用于跟蹤指向該對(duì)象的引用的數(shù)量。當(dāng)該計(jì)數(shù)為零時(shí),對(duì)象占用的內(nèi)存被釋放。
讓我們看一個(gè)簡(jiǎn)短的代碼示例來演示引用計(jì)數(shù)是如何工作的:
>>> import sys
>>> a = []
>>> b = a
>>> sys.getrefcount(a)
3
在上面的示例中,空列表對(duì)象[]
的引用計(jì)數(shù)是3。list對(duì)象被a
,b
引用,而傳遞給sys.getrefcount()。
參數(shù)返回到GIL:
問題是這個(gè)引用計(jì)數(shù)變量需要保護(hù),以避免兩個(gè)線程同時(shí)增加或減少其值的爭(zhēng)用條件。如果發(fā)生這種情況,可能會(huì)導(dǎo)致從未釋放的內(nèi)存泄漏,或者更糟的是,在對(duì)該對(duì)象的引用仍然存在時(shí)錯(cuò)誤地釋放內(nèi)存。這可能會(huì)導(dǎo)致Python程序崩潰或出現(xiàn)其他“奇怪”的錯(cuò)誤。
可以通過向線程間共享的所有數(shù)據(jù)結(jié)構(gòu)添加鎖來確保此引用計(jì)數(shù)變量的安全,這樣就不會(huì)對(duì)它們進(jìn)行不一致的修改。
但是向每個(gè)對(duì)象或?qū)ο蠼M添加一個(gè)鎖意味著將存在多個(gè)鎖,這可能會(huì)導(dǎo)致另一個(gè)問題死鎖(死鎖只能在有多個(gè)鎖的情況下發(fā)生)。另一個(gè)副作用是重復(fù)獲取和釋放鎖會(huì)導(dǎo)致性能下降。
GIL是解釋器本身的一個(gè)鎖,它添加了一個(gè)規(guī)則,即執(zhí)行任何Python字節(jié)碼都需要獲取解釋器鎖。這可以防止死鎖(因?yàn)橹挥幸粋€(gè)鎖),并且不會(huì)帶來太多的性能開銷。但是它有效地使任何CPU受限的Python程序成為單線程的。
GIL雖然被Ruby等其他語言的解釋器使用,但并不是解決這個(gè)問題的唯一方法。有些語言通過使用引用計(jì)數(shù)以外的方法(如垃圾收集)來避免線程安全內(nèi)存管理對(duì)GIL的要求。另一方面,這意味著這些語言通常必須通過添加其他性能提升功能(如JIT編譯器)來彌補(bǔ)GIL單線程性能優(yōu)勢(shì)的損失。
為什么選擇GIL作為解決方案?
那么,為什么在Python中使用了一種看起來非常困難的方法呢?Python的開發(fā)人員做出了一個(gè)錯(cuò)誤的決定嗎?用拉里·黑斯廷斯的話說,GIL的設(shè)計(jì)決定是使Python像今天一樣流行的原因之一。
Python從操作系統(tǒng)沒有線程概念的時(shí)候就已經(jīng)存在了。Python被設(shè)計(jì)為易于使用,以使開發(fā)更快,越來越多的開發(fā)人員開始使用它。
正在為現(xiàn)有的C庫編寫許多擴(kuò)展,Python需要這些庫的特性。為了防止不一致的更改,這些C擴(kuò)展需要GIL提供的線程安全內(nèi)存管理。
GIL易于實(shí)現(xiàn),并且很容易添加到Python中。它為單線程程序提供了性能提升,因?yàn)橹恍枰芾硪粋€(gè)鎖。
C非線程安全的庫變得更易于集成。這些C擴(kuò)展成為Python被不同社區(qū)采用的原因之一。
正如您所見,GIL是一個(gè)實(shí)用的解決方案,解決了Python開發(fā)人員在Python早期面臨的難題。
對(duì)多線程Python程序的影響
當(dāng)您查看一個(gè)典型的Python程序或任何計(jì)算機(jī)程序時(shí),在性能上受限于CPU的程序和受限于I/O的程序之間存在差異。
受限于CPU的程序是那些將CPU推向極限的程序。這包括進(jìn)行矩陣乘法、搜索、圖像處理等數(shù)學(xué)計(jì)算的程序。
I/O綁定程序是指花費(fèi)時(shí)間等待來自用戶、文件、數(shù)據(jù)庫、網(wǎng)絡(luò)的輸入/輸出的程序,I/O綁定的程序有時(shí)必須等待相當(dāng)長(zhǎng)的時(shí)間,直到它們從源代碼獲得所需的內(nèi)容,因?yàn)樵创a可能需要在輸入/輸出準(zhǔn)備就緒之前進(jìn)行自己的處理,例如,一個(gè)用戶正在考慮輸入提示或在自己的進(jìn)程中運(yùn)行的數(shù)據(jù)庫查詢中輸入什么。
讓我們看看一個(gè)簡(jiǎn)單的CPU限制程序,它執(zhí)行倒計(jì)時(shí):
# single_threaded.py
import time
from threading import Thread
COUNT = 50000000
def countdown(n):
while n>0:
n -= 1
start = time.time()
countdown(COUNT)
end = time.time()
print('Time taken in seconds -', end - start)
在我的4核系統(tǒng)上運(yùn)行這段代碼,得到以下輸出:
$ python single_threaded.py
Time taken in seconds - 6.20024037361145
現(xiàn)在我修改了使用兩個(gè)并行線程對(duì)相同的倒計(jì)時(shí)進(jìn)行編碼:
# multi_threaded.py
import time
from threading import Thread
COUNT = 50000000
def countdown(n):
while n>0:
n -= 1
t1 = Thread(target=countdown, args=(COUNT//2,))
t2 = Thread(target=countdown, args=(COUNT//2,))
start = time.time()
t1.start()
t2.start()
t1.join()
t2.join()
end = time.time()
print('Time taken in seconds -', end - start)
當(dāng)我再次運(yùn)行它時(shí):
$ python multi_threaded.py
Time taken in seconds - 6.924342632293701
正如您所看到的,兩個(gè)版本完成所需的時(shí)間幾乎相同。在多線程版本中,GIL阻止了CPU綁定的線程并行執(zhí)行。
GIL對(duì)I/O綁定的多線程程序的性能沒有太大影響,因?yàn)樵诰€程等待I/O時(shí)鎖是在線程之間共享的。
但是線程完全是CPU綁定的程序,例如。,使用線程部分處理圖像的程序不僅會(huì)由于鎖定而變?yōu)閱尉€程,而且會(huì)看到執(zhí)行時(shí)間的增加,如上面的示例所示,與編寫為完全單線程的場(chǎng)景相比。
這是鎖增加的獲取和釋放開銷的結(jié)果。
為什么GIL尚未移除?
Python的開發(fā)人員收到了很多關(guān)于這方面的抱怨,但是像Python這樣流行的語言不能帶來像刪除GIL那樣重要的改變,而不會(huì)導(dǎo)致向后不兼容問題。
顯然,GIL可以被刪除,開發(fā)人員和研究人員在過去已經(jīng)多次這樣做,但所有這些嘗試都打破了現(xiàn)有的C擴(kuò)展,這些擴(kuò)展嚴(yán)重依賴于GIL提供的解決方案。
當(dāng)然,GIL解決的問題還有其他解決方案,但其中一些解決方案降低了效率單線程和多線程I/O綁定程序的性能,其中一些太難了。畢竟,您不希望現(xiàn)有的Python程序在新版本發(fā)布后運(yùn)行得更慢,對(duì)吧?
Python的創(chuàng)建者和BDFL,Guido van Rossum,2007年9月,他在文章“移除GIL并不容易”中回答了社區(qū)的問題:“我希望在單線程程序(以及多線程但I(xiàn)/O綁定的程序)的性能沒有降低的情況下,我可以在Py3k中安裝一組補(bǔ)丁”。
而且這種情況還沒有得到解決通過此后的任何嘗試來完成。
為什么在python3中沒有刪除它?
python3確實(shí)有機(jī)會(huì)從頭開始很多特性,在這個(gè)過程中,它打破了一些現(xiàn)有的C擴(kuò)展,這些擴(kuò)展需要進(jìn)行更新和移植才能與python3一起使用。這就是為什么Python3的早期版本被社區(qū)采用的速度較慢的原因。
但是為什么沒有同時(shí)移除GIL呢?
刪除GIL會(huì)使python3在單線程性能上比python2慢,您可以想象這會(huì)導(dǎo)致什么結(jié)果。您不能否認(rèn)GIL的單線程性能優(yōu)勢(shì)。因此,結(jié)果是Python 3仍然具有GIL。
但是Python 3確實(shí)給現(xiàn)有的GIL帶來了重大改進(jìn)-
我們討論了GIL對(duì)“僅CPU綁定”和“僅I/O綁定”多線程程序的影響,但是對(duì)于一些線程是I/O綁定的程序和一些線程是CPU綁定的程序呢?在這樣的程序中,眾所周知Python的GIL會(huì)使I/O綁定的線程饑餓,因?yàn)闆]有給它們機(jī)會(huì)從CPU綁定的線程獲取GIL同一個(gè)線程可以繼續(xù)使用。
>>> import sys
>>> # The interval is set to 100 instructions:
>>> sys.getcheckinterval()
100
此機(jī)制中的問題是,大多數(shù)情況下,CPU綁定的線程會(huì)在其他線程獲取GIL之前重新獲取GIL本身。這是由David Beazley研究的,可視化可以在這里找到。
2009年,Antoine Pitrou在Python 3.2中修復(fù)了這個(gè)問題,他添加了一種機(jī)制,可以查看其他線程丟棄的GIL獲取請(qǐng)求的數(shù)量,并且不允許當(dāng)前線程在其他線程有機(jī)會(huì)獲取GIL之前重新獲取GIL運(yùn)行。
作者介紹
熱門博客推薦