全域性直譯器鎖GIL
解釋一下對GIL的理解?
GIL 又叫全域性直譯器鎖,首先說一點,Python語言與GIL全域性直譯器鎖沒有關係,僅僅是因為歷史原因,在cpython直譯器中還存在GIL難以移除。GIL是功能與效能權衡後的產物,它有著存在的合理性,也有著難以移除的歷史客觀因素。
為什麼存在GIL?
在早期的開發過程中,因為物理因素限制,從最開始的單核CPU發展為多核CPU, 想要充分發揮多核CPU的效能需要利用到多執行緒程式設計,Python中同樣引入了多執行緒程式設計,而多執行緒程式設計引入帶來的問題是:執行緒之間資料的一致性和狀態同步的問題。 而想解決這些問題最好的方法就是加一把鎖,所以就有了GIL全域性直譯器鎖這樣一把大鎖。
隨著越來越多的程式碼庫開發者接受GIL,而後逐漸大量依賴於這一特性進行開發,到最後發現GIL對多核CPU多執行緒程式設計的效率是低效影響的時候,想要移除這一特性,卻發現已經很難了。
談談GIL的作用?
第一個作用: 當前執行緒必須需要先獲取GIL,才能進入CPU執行程式碼。GIL的存在保障了在同一時刻只能有一個執行緒獲取GIL,執行程式碼。
第二個作用:當遇到IO阻塞時,執行執行緒會釋放GIL,給其他執行緒獲取鎖執行程式碼的機會。
問題: 如果是CPU密集型,一直佔有CPU,而沒有遇到IO阻塞,是不是其他執行緒就沒有機會執行?
其實也並不是這樣,在直譯器中會進行週期性的程式碼檢測和執行程式碼排程,在Python2中使用的是計數器的方式釋放GIL,就是當計數達到一定閥值,當前執行執行緒就會釋放GIL給其他執行緒執行的機會。但會出現的問題就是: 當前執行執行緒剛釋放了GIL可能又會立即再獲取GIL進行執行。 在Python3中使用的是計時器的方式釋放GIL,就是當前執行執行緒執行時間達到一定閥值就會釋放GIL,給其他執行緒執行的機會。這樣避免了當前執行執行緒剛釋放GIL又立即獲取的情況,同時線上程中增加了執行緒優先順序,高優先順序的執行緒可以迫使執行的執行緒釋放GIL,進行執行。
談談GIL的設計缺陷和影響?
在早期的開發過程中,為了讓各個執行緒能夠平均的利用CPU的執行時間,python中採用的是計數的方式切換執行程式碼,就是當計數(執行的執行緒程式碼數)達到一定的閥值,執行執行緒就會釋放GIL鎖,給其他執行緒執行的機會。這一模式在單核CPU中沒有問題,因為無論是其他哪個執行緒被喚醒,都能夠成功的獲取GIL進入CPU執行程式碼。而在多核CPU中則會有問題,當喚醒其它核心上的執行緒時候,大多數情況下總是當前主執行緒剛剛釋放GIL,又會立即再次獲取GIL進行執行,而其他被喚醒的執行緒只能白白等待浪費CPU的執行時間,等到執行時間結束,會進入到待喚醒待排程狀態,再次被喚醒,再次等待,如此惡性迴圈著。
GIL的影響有: GIL無疑是一把全域性排他鎖,它的存在保證了再同一時刻只能有一個執行緒獲取GIL進行執行程式碼,所以就無法讓多核CPU多執行緒實現並行,而想充分發揮多核CPU的最大效能就是實現多工並行。 下面解釋一下什麼是併發和並行。
併發: 當任務數大於CPU核心數時,總有一些任務是沒有在執行的,只不過是因為CPU的切換速度很快,讓人感覺像是多工同時在一起執行。
並行: 當任務數小於或者等於CPU核心數時,每個任務都有一個對應的核來處理執行,是真正意義上的多工同時一起執行。
如何避免GIL的影響?
方法一: 更換python直譯器,比如jpython,用java開發的python直譯器。 但因為眾多的庫都是建立在GIL這一特性下開發的,所以更換直譯器很多庫用不了,不划算。
方法二: 使用多程序代替多執行緒。 multiprocession庫的開發很大程度上就是為了彌補threading庫因為GIL特性低效的缺陷,它完整的複製了一份threading裡面的API介面便於遷移管理。 唯一的不同就是它是多程序而不是多執行緒,每一個程序都有自己的GIL鎖,不會出現程序直接GIL鎖的競爭。而多執行緒的時候則會出現釋放GIL多個執行緒同時爭搶鎖的情況,這樣會浪費CPU的效能資源。
但多程序也不是萬良解藥,它的引入同時會增加實現程序間通訊和狀態同步的難度,在多執行緒中對公共資源進行修改,只需要線上程中gloab 宣告一下就可以了,多執行緒之間是共享全域性變數的。而在多程序中,則需要使用一個Queue佇列,通過put 或者get來傳遞資料,增加了開發的難度。多程序間是不共享全域性變數的。