怎么并線(三條電線怎么并線)
1166
2022-05-30
簡而言之,Python全局解釋器鎖或GIL是一種互斥鎖(或鎖),僅允許一個線程持有Python解釋器的控制權。
這意味著在任何時間點只有一個線程可以處于執行狀態。對于執行單線程程序的開發人員而言,GIL的影響并不明顯,但它可能是CPU綁定和多線程代碼的性能瓶頸。
由于即使在具有多個CPU內核的多線程體系結構中,GIL一次一次只允許執行一個線程,因此GIL被譽為Python的“臭名昭著”功能。
在本文中,您將學習GIL如何影響Python程序的性能,以及如何減輕GIL對代碼的影響。
為什么Cpython需要GIL?
Python使用引用計數進行內存管理。
譯注:還有標記清除和分代回收
這意味著用Python創建的對象具有引用計數變量,該變量跟蹤指向該對象的引用數。當此計數達到零時,將釋放對象占用的內存。
讓我們看一個簡短的代碼示例,以演示引用計數的工作原理:
>>> import sys >>> a = [] >>> b = a >>> sys.getrefcount(a) 3
1
2
3
4
5
在上面的示例中,空列表對象的引用計數為[]3。列表對象被引用a,b并且參數傳遞給sys.getrefcount()。
回到GIL:
問題在于該引用計數變量需要保護,以防止兩個線程同時增加或減少其值的競爭狀態。如果發生這種情況,則可能導致從未釋放的內存泄漏,或者更糟糕的是,在仍然存在對該對象的引用的情況下,錯誤地釋放了內存。這可能會導致崩潰或Python程序中的其他“怪異”錯誤。
通過將鎖添加到跨線程共享的所有數據結構中,以確保它們不會被不一致地修改,可以保持此引用計數變量的安全。
但是,將鎖添加到每個對象或對象組意味著將存在多個鎖,這可能會引起另一個問題-死鎖(死鎖只有在有多個鎖的情況下才會發生)。另一個副作用是由于重復獲取和釋放鎖而導致性能降低。
GIL是解釋器本身的單一鎖,它添加了一個規則,即任何Python字節碼的執行都需要獲取解釋器鎖。這樣可以防止死鎖(因為只有一個鎖)并且不會帶來太多的性能開銷。但這有效地使所有受CPU約束的Python程序成為單線程。
盡管解釋器用于其他語言(例如Ruby),但GIL并不是解決此問題的唯一方法。某些語言通過使用引用計數以外的方法(例如垃圾回收)來避免對線程安全的內存管理使用GIL的要求。
另一方面,這意味著這些語言通常必須通過添加其他性能提升功能(例如JIT編譯器)來彌補GIL的單線程性能優勢的損失。
為什么選擇GIL作為解決方案?
那么,為什么在Python中使用了一種看起來如此阻礙的方法呢?Python開發人員是否會做出錯誤的決定?
好吧,用Larry Hastings的話來說, GIL的設計決定是使Python像今天一樣流行的原因之一。
自從操作系統沒有線程概念以來,Python就已經存在了。Python被設計為易于使用,以加快開發速度,越來越多的開發人員開始使用它。
現有的C庫正在編寫許多擴展,這些C需要Python中的功能。為了防止不一致的更改,這些C擴展需要GIL提供的線程安全內存管理。
GIL易于實現,并且很容易添加到Python中。由于只需要管理一個鎖,因此它可以提高單線程程序的性能。
非線程安全的C庫變得易于集成。這些C擴展成為Python被不同社區輕易采用的原因之一。
如您所見,GIL是CPython開發人員在Python生命早期面臨的一個難題的實用解決方案。
對多線程Python程序的影響
當您查看典型的Python程序(或與此相關的任何計算機程序)時,在性能上受CPU限制的程序與受I / O限制的程序之間是有區別的。
受CPU約束的程序是將CPU推到極限的程序。這包括進行數學計算的程序,例如矩陣乘法,搜索,圖像處理等。
受I / O約束的程序是花費時間等待輸入/輸出的程序,它可能來自用戶,文件,數據庫,網絡等。受I / O約束的程序有時必須等待大量的時間,直到它們進入由于源可能需要在輸入/輸出準備好之前進行自己的處理,因此可以從源那里獲得他們需要的東西,例如,用戶考慮要在輸入提示中輸入什么內容或在其輸入中運行數據庫查詢自己的過程。
讓我們看一個執行倒計時的簡單的受CPU約束的程序:
# single_threaded.py import time 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)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
輸出結果:Time taken in seconds: 2.232430934906006
現在,我使用兩個并行線程對代碼進行了一些修改,以實現相同的倒計時:
# 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)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
再次運行:Time taken in seconds: 2.353055953979492
如您所見,兩個版本花費的時間幾乎相同。在多線程版本中,GIL阻止CPU綁定的線程并行執行。
GIL對受I / O綁定的多線程程序的性能影響不大,因為在線程等待I / O時它們之間共享鎖。
但是,如上例所示,線程完全受CPU約束的程序(例如使用線程處理映像的程序)不僅會由于鎖定而變為單線程,而且執行時間也會增加。與將其編寫為完全單線程的方案相比。
這種增加是鎖增加了獲取和釋放開銷的結果。
為什么還沒有刪除GIL?
Python的開發人員對此有很多抱怨,但是像Python這樣流行的語言在不引起向后不兼容的問題的情況下,不能帶來與刪除GIL一樣大的變化。
GIL顯然可以刪除,并且開發人員和研究人員過去已經做過多次,但是所有這些嘗試都破壞了現有的C擴展,這些擴展在很大程度上取決于GIL提供的解決方案。
當然,對于GIL解決的問題,還有其他解決方案,但是其中一些降低了單線程和多線程I / O綁定程序的性能,其中有些太困難了。畢竟,您不希望現有的Python程序在新版本發布后運行速度變慢,對吧?
Python的創建者和BDFL的Guido van Rossum在2007年9月的文章“刪除GIL并不容易”中向社區做出了回答:
“ 只有在單線程程序(以及多線程但受I / O綁定的程序)的性能不降低的情況下,我才歡迎在Py3k中安裝一組補丁程序”
此后的任何嘗試都沒有滿足此條件。
為什么在Python 3中未將其刪除?
Python 3確實有機會從頭開始啟動許多功能,并且在此過程中破壞了一些現有的C擴展,這些擴展隨后需要進行更新并移植到Python 3才能使用。這就是早期版本的原因。 Python 3的社區采用速度較慢。
但是為什么不將GIL一起刪除呢?
與單線程性能相比,刪除GIL會使Python 3的速度比Python 2慢,并且您可以想象會導致什么。您無法反對GIL的單線程性能優勢。因此,結果是Python 3仍然具有GIL。
但是Python 3確實對現有GIL進行了重大改進-
我們討論了GIL對“僅CPU綁定”和“僅I / O綁定”多線程程序的影響,但是其中一些線程受I / O綁定而某些線程受CPU綁定的程序又如何呢?
在這樣的程序中,眾所周知,Python的GIL使得I / O綁定線程饑餓,因為它們沒有機會從CPU綁定線程獲取GIL。
這是因為Python內置了一種機制,該機制強制線程在固定的連續使用時間間隔后釋放GIL ,如果沒有其他人獲得GIL,則同一線程可以繼續使用它。
>>> import sys >>> sys.getswitchinterval() 0.005
1
2
3
這種機制的問題在于,在大多數情況下,CPU綁定線程會在其他線程無法獲取GIL之前重新獲取GIL本身。這是David Beazley進行的研究,可以在此處找到可視化效果。
這個問題在2009年的Python 3.2中由Antoine Pitrou修復,他添加了一種機制來查看被丟棄的其他線程的GIL獲取請求的數量,并且不允許當前線程在其他線程有機會運行之前重新獲取GIL。
如何處理Python的GIL
如果GIL導致您遇到問題,請嘗試以下幾種方法:
多進程與多線程:最流行的方法是使用多進程方法,其中您使用多個進程而不是線程。每個Python進程都有自己的Python解釋器和內存空間,因此GIL不會成為問題。Python有一個multiprocessing模塊,可以讓我們輕松地創建如下過程:
from multiprocessing import Pool import time COUNT = 50000000 def countdown(n): while n > 0: n -= 1 if __name__ == '__main__': pool = Pool(processes=2) start_time = time.time() r1 = pool.apply_async(countdown, [COUNT//2]) r2 = pool.apply_async(countdown, [COUNT//2]) pool.close() pool.join() end_time = time.time() print('Time taken in second:', end_time-start_time)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
輸出結果:Time taken in second: 1.3340442180633545
與多線程版本相比,性能提高了,對嗎?
時間并沒有減少到我們上面看到的一半,因為流程管理有其自己的開銷。多個進程比多個線程重,因此請記住,這可能會成為擴展瓶頸。
備選的Python解釋器: Python具有多種解釋器實現。最受歡迎的分別是用C,Java,C#和Python編寫的CPython,Jython,IronPython和PyPy。GIL僅存在于原始Python實現中,即CPython。如果您的程序及其庫可用于其他實現之一,則也可以嘗試一下。
**稍后:**許多Python用戶都利用了GIL的單線程性能優勢。多線程程序員不必煩惱,因為Python社區中一些最聰明的人正在努力從CPython中刪除GIL。一種這樣的嘗試被稱為“ Gilectomy”。
Python GIL通常被認為是一個神秘而困難的話題。但是請記住,作為Pythonista,通常只有在編寫C擴展或在程序中使用CPU綁定多線程時才受到它的影響。
在這種情況下,本文應為您提供了解GIL以及在您自己的項目中如何處理GIL所需的一切。而且,如果您想了解GIL的底層內部工作原理,建議您觀看David Beazley 的“ 了解Python GIL”演講。
原文出處:https://realpython.com/python-gil/
Python 任務調度
版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。