GIL的存在一直是富有爭議的,它導致Python程序無法真正利用現(xiàn)代操作系統(tǒng)的多進程特性。需要注意的是,對于I/O圖形處理、NumPy數(shù)學計算這樣的耗時操作都發(fā)生在GIL之外,實際上基本不受影響,真正受影響的都是Python字節(jié)碼的執(zhí)行,GIL會導致性能瓶頸的出現(xiàn)。總之,只有在使用純Python做CPU密集的多線程運算時GIL會是問題。
GIL是什么
Python的代碼執(zhí)行由Python虛擬機(也叫解釋器主循環(huán),CPython版本)來控制,Python在設計之初就考慮到在解釋器的主循環(huán)中,同時只有一個線程在運行。即每個CPU在任意時刻只有一個線程在解釋器中運行。對Python虛擬機訪問的控制由全局解釋鎖GIL控制,正是這個鎖來控制同一時刻只有一個線程能夠運行。——在單核CPU下的多線程其實都只是并發(fā),不是并行。
并發(fā):兩個或多個事件在同一時間間隔發(fā)生,或者說交替做不同事件的能力,或者說不同的代碼塊交替執(zhí)行。 并行:兩個或者多個事件在同一時刻發(fā)生,或者說同時做不同事件的能力,或者說不同的代碼塊同時執(zhí)行。
并發(fā)和并行都可以處理“多任務”,二者的主要區(qū)別在于是否是“同時進行”多個的任務。但是涉及到任務分解(有先后依賴耦合度高的任務無法做到并行)、任務運行(可能要考慮互斥、鎖、共享等)、結果合并。
Python下的多線程
在Python多線程下,每個線程的執(zhí)行方式,如下:
- 獲取GIL
- 切換到這個線程去執(zhí)行
- 運行代碼,這里有兩種機制:
- 指定數(shù)量的字節(jié)碼指令(100個)
- 固定時間15ms線程主動讓出控制
- 把線程設置為睡眠狀態(tài)
- 釋放GIL
- 再次重復以上步驟
在Python2中,在解釋器解釋執(zhí)行任何Python代碼時,都需要先獲得這把鎖才行(同一時間只會有一個獲得了GIL的線程在跑,其它的線程都處于等待狀態(tài)等著GIL的釋放),在遇到I/O操作時會釋放這把鎖。如果是純計算的程序,沒有I/O操作,解釋器會每隔100次操作就釋放這把鎖,讓別的線程有機會執(zhí)行(這個次數(shù)可以通過sys.setcheckinterval來調(diào)整)也正是這種設定,是的多線程的CPU密集型計算非常雞肋,下面會講到為何如此。
而在python3中,GIL不使用ticks計數(shù)(100次,釋放GIL),改為使用計時器(執(zhí)行時間達到15ms閾值后,當前線程釋放GIL),使得執(zhí)行計算的次數(shù)更多,釋放次數(shù)減少,這樣對CPU密集型程序更加友好,但依然沒有解決GIL導致的同一時間只能執(zhí)行一個線程的問題,所以效率依然不盡如人意。
那么是不是Python的多線程是雞肋嘛?
CPU密集型(各種循環(huán)處理、計數(shù)等等),在這種情況下,ticks計數(shù)很快就會達到閾值,然后觸發(fā)GIL的釋放與再競爭(多個線程來回切換是需要消耗資源的),所以python下的多線程對CPU密集型代碼并不友好,會觸發(fā)相當頻繁的線程切換。
IO密集型(文件處理、網(wǎng)絡爬蟲等),多線程能夠有效提升效率(單線程下有IO操作會進行IO等待,造成不必要的時間浪費,而開啟多線程能在線程A等待時,自動切換到線程B,可以不浪費CPU的資源,從而能提升程序執(zhí)行效率,一個線程獲得GIL發(fā)送消息,然后等待返回消息(阻塞),Python此時釋放GIL,其他線程得到GIL發(fā)送消息,然后同樣等待返回消息(阻塞)......,這樣保證了IO傳輸過程時間的合理利用,減少了IO等待造成的資源浪費,提高IO傳輸效率)。所以python的多線程對IO密集型代碼比較友好。
有哪些結論?
I/O密集型使用多線程并發(fā)執(zhí)行提高效率、計算密集型使用多進程(multiprocessing)并行執(zhí)行提高效率。通常程序既包含IO操作又包含計算操作,那么這種情況下,在開始并發(fā)任務之前,可以先進行測試,測試多線程、多進程哪個效率高就是用哪種方式。
請注意:多核多線程比單核多線程更差,多核多進程下,CPU1釋放GIL后,其他CPU上的線程都會進行競爭,但GIL可能會馬上又被CPU1拿到,CPU2釋放GIL后……,導致其他幾個CPU上被喚醒后的線程會醒著等待到切換時間后又進入待調(diào)度狀態(tài),這樣會造成線程顛簸(thrashing),導致效率更低。
多線程下的CPU密集型計算也不是無藥可醫(yī),可以利用ctypes繞過GIL,ctypes可以使py直接調(diào)用任意的C動態(tài)庫的導出函數(shù)。所要做的只是把關鍵部分用C/C++寫成Python擴展。而且,ctypes會在調(diào)用C函數(shù)前釋放GIL。
同時,可以了解下協(xié)程,又稱微線程。
協(xié)程最大的優(yōu)勢就是協(xié)程極高的執(zhí)行效率。因為子程序切換不是線程切換,而是由程序自身控制,因此,沒有線程切換的開銷,和多線程比,線程數(shù)量越多,協(xié)程的性能優(yōu)勢就越明顯。
第二大優(yōu)勢就是不需要多線程的鎖機制,因為只有一個線程,也不存在同時寫變量沖突,在協(xié)程中控制共享資源不加鎖,只需要判斷狀態(tài)就好了,所以執(zhí)行效率比多線程高很多。
因為協(xié)程是一個線程執(zhí)行,那怎么利用多核CPU呢?最簡單的方法是多進程+協(xié)程,既充分利用多核,又充分發(fā)揮協(xié)程的高效率,可獲得極高的性能。