關於我
編程界的一名小程序猿,目前在一個創業團隊任team lead,技術棧涉及Android、Python、Java和Go,這個也是咱們團隊的主要技術棧。 聯繫:hylinux1024@gmail.comhtml
A global interpreter lock (GIL) is a mechanism used in computer-language interpreters to synchronize the execution of threads so that only one native thread can execute at a time. --引用自wikipediapython
從上面的定義能夠看出,GIL
是計算機語言解析器用於同步線程執行的一種同步鎖機制。不少編程語言都有GIL
,例如Python
、Ruby
。linux
Python
做爲一種面向對象的動態類型編程語言,開發者編寫的代碼是經過解析器順序解析執行的。 大多數人目前使用的Python
解析器是CPython
提供的,而CPython
的解析器是使用引用計數來進行內存管理,爲了對多線程安全的支持,引用了global intepreter lock
,只有獲取到GIL
的線程才能執行。若是沒有這個鎖,在多線程編碼中即便是簡單的操做也會引發共享變量被多個線程同時修改的問題。例若有兩個線程同時對同一個對象進行引用時,這兩個線程都會將變量的引用計數從0增長爲1,明顯這是不正確的。
能夠經過sys
模塊獲取一個變量的引用計數git
>>> import sys
>>> a = []
>>> sys.getrefcount(a)
2
>>> b = a
>>> sys.getrefcount(a)
3
複製代碼
sys.getrefcount()
方法中的參數對a的引用也會引發計數的增長。github
是否能夠對每一個變量都分別使用鎖來同步呢?數據庫
若是有多個鎖的話,線程同步時就容易出現死鎖,並且編程的複雜度也會上升。當全局只有一個鎖時,全部線程都在競爭一把鎖,就不會出現相互等待對方鎖的狀況,編碼的實現也更簡單。此外只有一把鎖時對單線程的影響其實並非很大。編程
Python
核心開發團隊以及Python
社區的技術專家對移除GIL
也作過屢次嘗試,然而最後都沒有令各方滿意的方案。小程序
內存管理技術除了引用計數外,一些編程語言爲了不引用全局解析鎖,內存管理就使用垃圾回收機制。安全
固然這也意味着這些使用垃圾回收機制的語言就必須提高其它方面的性能(例如JIT
編譯),來彌補單線程程序的執行性能的損失。
對於Python
的來講,選擇了引用計數做爲內存管理。一方面保證了單線程程序執行的性能,另外一方面GIL
使得編碼也更容易實現。
在Python
中不少特性是經過C
庫來實現的,而在C
庫中要保證線程安全的話也是依賴於GIL
。bash
因此當有人成功移除了GIL
以後,Python
的程序並無變得更快,由於大多數人使用的都是單線程場景。
首先來GIL
對IO
密集型程序和CPU
密集型程序的的區別。 像文件讀寫、網絡請求、數據庫訪問等操做都是IO
密集型的,它們的特色須要等待IO
操做的時間,而後才進行下一步操做;而像數學計算、圖片處理、矩陣運算等操做則是CPU
密集型的,它們的特色是須要大量CPU
算力來支持。
對於IO
密集型操做,當前擁有鎖的線程會先釋放鎖,而後執行IO
操做,最後再獲取鎖。線程在釋放鎖時會把當前線程狀態存在一個全局變量PThreadState
的數據結構中,當線程獲取到鎖以後恢復以前的線程狀態
用文字描述執行流程
保存當前線程的狀態到一個全局變量中
釋放GIL
... 執行IO操做 ...
獲取GIL
從全局變量中恢復以前的線程狀態
複製代碼
下面這段代碼是測試單線程執行500萬次消耗的時間
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)
# 執行結果
# Time taken in seconds - 2.44541597366333
複製代碼
在個人8核的macbook
上跑大約是2.4秒,而後再看一個多線程版本
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)
# 執行結果
# Time taken in seconds - 2.4634649753570557
複製代碼
上文代碼每一個線程都執行250萬次,若是線程是併發的,執行時間應該是上面單線程版本的一半時間左右,然而在我電腦中執行時間大約爲2.5秒! 多線程不但沒有更高效率,反而還更耗時了。這個例子就說明Python
中的線程是順序執行的,只有獲取到鎖的線程能夠獲取解析器的執行時間。多線程執行多出來的那點時間就是獲取鎖和釋放鎖消耗的時間。
那如何實現高併發呢?
答案是使用多進程。前面的文章有介紹多進程的使用
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()
r1 = pool.apply_async(countdown, [COUNT // 2])
r2 = pool.apply_async(countdown, [COUNT // 2])
pool.close()
pool.join()
end = time.time()
print('Time taken in seconds -', end - start)
# 執行結果
# Time taken in seconds - 1.2389559745788574
複製代碼
使用多進程,每一個進程運行250萬次,大約消耗1.2秒的時間。差很少是上面線程版本的一半時間。
固然還可使用其它Python
解析器,例如Jython
、IronPython
或PyPy
。
既然每一個線程執行前都要獲取鎖,那麼有一個線程獲取到鎖一直佔用不釋放,怎麼辦?
IO
密集型的程序會主動釋放鎖,但對於CPU
密集型的程序或IO
密集型和CPU
混合的程序,解析器將會如何工做呢?
早期的作法是Python
會執行100條指令後就強制線程釋放GIL
讓其它線程有可執行的機會。
能夠經過如下獲取到這個配置
>>> import sys
>>> sys.getcheckinterval()
100
複製代碼
在個人電腦中還打印了下面的輸出警告
Warning (from warnings module):
File "__main__", line 1
DeprecationWarning: sys.getcheckinterval() and sys.setcheckinterval() are deprecated. Use sys.getswitchinterval() instead.
複製代碼
意思是sys.getcheckinterval()
方法已經廢棄,應該使用sys.getswitchinterval()
方法。 由於傳統的實現中每解析100指令的就強制線程釋放鎖的作法,會致使CPU
密集型的線程會一直佔用GIL
而IO
密集型的線程會一直得不到解析的問題。因而新的線程切換方案就被提出來了
>>> sys.getswitchinterval()
0.005
複製代碼
這個方法返回0.05秒,意思是每一個線程執行0.05秒後就釋放GIL
,用於線程的切換。
在CPython
解析器的實現因爲global interpreter lock
(全局解釋鎖)的存在,任什麼時候刻都只有一個線程能執行Python
的bytecode
(字節碼)。
常見的內存管理方案有引用計數和垃圾回收,Python
選擇了前者,這保證了單線程的執行效率,同時對編碼實現也更加簡單。想要移除GIL
是不容易的,即便成功將GIL
去除,對Python
的來講是犧牲了單線程的執行效率。
Python
中GIL
對IO
密集型程序能夠較好的支持多線程併發,然而對CPU
密集型程序來講就要使用多進程或使用其它不使用GIL
的解析器。
目前最新的解析器實現中線程每執行0.05秒就會強制釋放GIL
,進行線程的切換。