你是否真的瞭解全局解析鎖(GIL)

關於我
編程界的一名小程序猿,目前在一個創業團隊任team lead,技術棧涉及Android、Python、Java和Go,這個也是咱們團隊的主要技術棧。 聯繫:hylinux1024@gmail.comhtml

0x00 什麼是全局解析鎖(GIL)

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,例如PythonRubylinux

0x01 爲何會有GIL

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

是否能夠對每一個變量都分別使用鎖來同步呢?數據庫

若是有多個鎖的話,線程同步時就容易出現死鎖,並且編程的複雜度也會上升。當全局只有一個鎖時,全部線程都在競爭一把鎖,就不會出現相互等待對方鎖的狀況,編碼的實現也更簡單。此外只有一把鎖時對單線程的影響其實並非很大。編程

0x02 能夠移除GIL嗎?

Python核心開發團隊以及Python社區的技術專家對移除GIL也作過屢次嘗試,然而最後都沒有令各方滿意的方案。小程序

內存管理技術除了引用計數外,一些編程語言爲了不引用全局解析鎖,內存管理就使用垃圾回收機制。安全

固然這也意味着這些使用垃圾回收機制的語言就必須提高其它方面的性能(例如JIT編譯),來彌補單線程程序的執行性能的損失。
對於Python的來講,選擇了引用計數做爲內存管理。一方面保證了單線程程序執行的性能,另外一方面GIL使得編碼也更容易實現。
Python中不少特性是經過C庫來實現的,而在C庫中要保證線程安全的話也是依賴於GILbash

因此當有人成功移除了GIL以後,Python的程序並無變得更快,由於大多數人使用的都是單線程場景。

0x03 對多線程程序的影響

首先來GILIO密集型程序和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解析器,例如JythonIronPythonPyPy

既然每一個線程執行前都要獲取鎖,那麼有一個線程獲取到鎖一直佔用不釋放,怎麼辦?

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密集型的線程會一直佔用GILIO密集型的線程會一直得不到解析的問題。因而新的線程切換方案就被提出來了

>>> sys.getswitchinterval()
0.005
複製代碼

這個方法返回0.05秒,意思是每一個線程執行0.05秒後就釋放GIL,用於線程的切換。

0x04 總結

CPython解析器的實現因爲global interpreter lock(全局解釋鎖)的存在,任什麼時候刻都只有一個線程能執行Pythonbytecode(字節碼)。
常見的內存管理方案有引用計數和垃圾回收,Python選擇了前者,這保證了單線程的執行效率,同時對編碼實現也更加簡單。想要移除GIL是不容易的,即便成功將GIL去除,對Python的來講是犧牲了單線程的執行效率。
PythonGILIO密集型程序能夠較好的支持多線程併發,然而對CPU密集型程序來講就要使用多進程或使用其它不使用GIL的解析器。
目前最新的解析器實現中線程每執行0.05秒就會強制釋放GIL,進行線程的切換。

0x05 爲了看懂GIL我閱讀了下面這些資料

相關文章
相關標籤/搜索