深入理解python虛擬機(jī)GIL詳解
選擇 GIL 的原因
GIL 對(duì) Python 代碼的影響
簡(jiǎn)單來(lái)說(shuō),Python 全局解釋器鎖或 GIL 是一個(gè)互斥鎖,只允許一個(gè)線程保持 Python 解釋器的控制權(quán),也就是說(shuō)在同一個(gè)時(shí)刻只能夠有一個(gè)線程執(zhí)行 Python 代碼,如果整個(gè)程序是單線程的話,這也無(wú)傷大雅,但是如果你的程序是多線程計(jì)算密集型的程序的話,這對(duì)程序的影響就很大了。
因?yàn)檎麄€(gè)虛擬機(jī)都有一把大鎖進(jìn)行保護(hù),所以虛擬的代碼就可以認(rèn)為是單線程執(zhí)行的,因此不需要做線程安全的防護(hù),直接按照單線程的邏輯就行了。不僅僅是虛擬機(jī),Python 層面的代碼也是這樣,對(duì)于有些 Python 層面的多線程代碼也可以不用鎖保護(hù),因?yàn)楸旧砭褪蔷€程安全的:
import threading data = [] def add_data(n): for i in range(n): data.append(i) if __name__ == '__main__': ts = [threading.Thread(target=add_data, args=(10,)) for _ in range(10)] for t in ts: t.start() for t in ts: t.join() print(data) print(len(data)) print(sum(data))
在上面的代碼當(dāng)中,當(dāng)程序執(zhí)行完之后 len(data)
的值永遠(yuǎn)都是 100,sum(data)
的值永遠(yuǎn)都是 450,因?yàn)樯厦娴拇a是線程安全的,可能你會(huì)有所疑惑,上面的代碼啟動(dòng)了 10 個(gè)線程同時(shí)往列表當(dāng)中增加數(shù)據(jù),如果兩個(gè)線程同時(shí)增加數(shù)據(jù)的時(shí)候就有可能存在線程之間覆蓋的情況,最終的 len(data)
的長(zhǎng)度應(yīng)該小于 100 ?
上面的代碼之所以是線程安全的原因是因?yàn)?data.append(i)
執(zhí)行 append 只需要虛擬機(jī)的一條字節(jié)碼,而在前面介紹 GIL 時(shí)候已經(jīng)談到了,每個(gè)時(shí)刻只能夠有一個(gè)線程在執(zhí)行虛擬機(jī)的字節(jié)碼,這就保證了每個(gè) append 的操作都是原子的,因?yàn)橹挥幸粋€(gè) append 操作執(zhí)行完成之后其他的線程才能夠執(zhí)行 append 操作。
我們來(lái)看一下上面程序的字節(jié)碼:
5 0 LOAD_GLOBAL 0 (range) 2 LOAD_FAST 0 (n) 4 CALL_FUNCTION 1 6 GET_ITER >> 8 FOR_ITER 14 (to 24) 10 STORE_FAST 1 (i) 6 12 LOAD_GLOBAL 1 (data) 14 LOAD_METHOD 2 (append) 16 LOAD_FAST 1 (i) 18 CALL_METHOD 1 20 POP_TOP 22 JUMP_ABSOLUTE 8 >> 24 LOAD_CONST 0 (None) 26 RETURN_VALUE
在上面的字節(jié)碼當(dāng)中 data.append(i)
對(duì)應(yīng)的字節(jié)碼為 (14, 16, 18) 這三條字節(jié)碼,而 (14, 16) 是不會(huì)產(chǎn)生數(shù)據(jù)競(jìng)爭(zhēng)的問(wèn)題的,因?yàn)樗皇羌虞d對(duì)象的方法和局部變量 i
的值,讓 append 執(zhí)行的方法是字節(jié)碼 CALL_METHOD,而同一個(gè)時(shí)刻只能夠有一個(gè)字節(jié)碼在執(zhí)行,因此這條字節(jié)碼也是線程安全的,所以才會(huì)有上面的代碼是線程安全的情況出現(xiàn)。
我們?cè)賮?lái)看一個(gè)非線程安全的例子:
import threading data = 0 def add_data(n): global data for i in range(n): data += 1 if __name__ == '__main__': ts = [threading.Thread(target=add_data, args=(100000,)) for _ in range(20)] for t in ts: t.start() for t in ts: t.join() print(data)
在上面的代碼當(dāng)中對(duì)于 data += 1 這個(gè)操作就是非線程安全的,因?yàn)檫@行代碼匯編編譯成 3 條字節(jié)碼:]
9 12 LOAD_GLOBAL 1 (data) 14 LOAD_CONST 1 (1) 16 INPLACE_ADD
首先 LOAD_GLOBAL,加載 data 數(shù)據(jù),LOAD_CONST 加載常量 1,最后執(zhí)行 INPLACE_ADD 進(jìn)行加法操作,這就可能出現(xiàn)線程1執(zhí)行完 LOAD_GLOBAL 之后,線程 2 連續(xù)執(zhí)行 3 條字節(jié)碼,那么這個(gè)時(shí)候 data 的值已經(jīng)發(fā)生變化了,而線程 1 拿的還是舊的數(shù)據(jù),因此最終執(zhí)行的之后會(huì)出現(xiàn)線程不安全的情況。(實(shí)際上虛擬機(jī)在執(zhí)行的過(guò)程當(dāng)中,發(fā)生數(shù)據(jù)競(jìng)爭(zhēng)比這個(gè)復(fù)雜很多,這里只是簡(jiǎn)單說(shuō)明一下)
GIL 對(duì)于虛擬機(jī)的影響
除了上面 GIL 對(duì)于 Python 代碼層面的影響,GIL 對(duì)于虛擬機(jī)來(lái)說(shuō)還有一個(gè)非常好的作用就是他不會(huì)讓虛擬機(jī)產(chǎn)生死鎖的現(xiàn)象,因?yàn)檎麄€(gè)虛擬機(jī)只有一把鎖??。
對(duì)于虛擬機(jī)的內(nèi)存管理和垃圾回收來(lái)說(shuō),GIL 可以說(shuō)極大的簡(jiǎn)化了 CPython 內(nèi)部的內(nèi)存管理和垃圾回收的實(shí)現(xiàn)。我們現(xiàn)在舉一個(gè)內(nèi)存管理和垃圾回收的多線程情況會(huì)出現(xiàn)數(shù)據(jù)競(jìng)爭(zhēng)的場(chǎng)景:
在 Python 當(dāng)中的垃圾回收是采用引用計(jì)數(shù)的方式進(jìn)行處理,如果沒(méi)有 GIL 那么就會(huì)存在多個(gè)線程同時(shí)對(duì)一個(gè) CPython 對(duì)象的引用計(jì)數(shù)進(jìn)行增加,而現(xiàn)在因?yàn)?GIL 的存在也就不需要進(jìn)行考慮這個(gè)問(wèn)題了。
另外一個(gè)比較重要的場(chǎng)景就是內(nèi)存的申請(qǐng)和釋放:在虛擬機(jī)內(nèi)部并不是直接調(diào)用 malloc 進(jìn)行實(shí)現(xiàn)的,在 CPython 內(nèi)部自己實(shí)現(xiàn)了一個(gè)內(nèi)存池進(jìn)行內(nèi)存的申請(qǐng)和釋放(這么做的原因主要是節(jié)省內(nèi)存),因?yàn)槭亲约簩?shí)現(xiàn)內(nèi)存池,因此需要保證線程安全,而現(xiàn)在因?yàn)橛?GIL 的存在,虛擬機(jī)實(shí)現(xiàn)內(nèi)存池只需要管單線程的情況,所以使得整個(gè)內(nèi)存管理變得更加簡(jiǎn)單。
GIL 對(duì)與 Python 的第三方 C 庫(kù)開(kāi)發(fā)人員來(lái)說(shuō)也是非常友好的,當(dāng)他們?cè)谶M(jìn)行第三方庫(kù)開(kāi)發(fā)的時(shí)候不需要去考慮在修改 CPython 對(duì)象的線程安全問(wèn)題,因?yàn)橐呀?jīng)有 GIL 了。從這個(gè)角度來(lái)說(shuō) GIL 在一定程度上推動(dòng)了 Python 的發(fā)展和普及。
GIL 帶來(lái)的問(wèn)題
GIL 帶來(lái)的最主要的問(wèn)題就是當(dāng)你的程序是計(jì)算密集型的時(shí)候,比如數(shù)學(xué)計(jì)算、圖像處理,GIL 就會(huì)帶來(lái)性能問(wèn)題,因?yàn)樗麩o(wú)法在同一個(gè)時(shí)刻跑多個(gè)線程。
之所以沒(méi)有在 Python 當(dāng)中刪除 GIL,最主要的原因就是目前很多 CPython 第三方庫(kù)是依賴(lài) GIL 這個(gè)特性的,如果直接在虛擬機(jī)層面移除 GIL,就會(huì)破壞 CPython C-API 的兼容性,這會(huì)導(dǎo)致很多依賴(lài) GIL 的第三方 C 庫(kù)發(fā)生錯(cuò)誤。而向后兼容這個(gè)特性對(duì)于社區(qū)來(lái)說(shuō)非常重要,這就是目前 CPython 還保留 GIL 最主要的原因。
GIL 源代碼分析
在本小節(jié)當(dāng)中為了更好的說(shuō)明 GIL 的設(shè)計(jì)和源代碼分析,本小節(jié)使用 CPython2.7.6 的 GIL 源代碼進(jìn)行分析(這種實(shí)現(xiàn)方式在 Python 3.2 以后被優(yōu)化改進(jìn)了,在本文當(dāng)中先不提及),我還翻了一下更早的 CPython 源代碼,都是使用這種方式實(shí)現(xiàn)的,可能細(xì)節(jié)方面可以會(huì)有點(diǎn)差異,我們現(xiàn)在來(lái)分析一下 GIL 具體是如何實(shí)現(xiàn)的,下面的代碼是一 GIL 加鎖和解鎖的代碼以及鎖的數(shù)據(jù)結(jié)構(gòu)表示:
// PyThread_type_lock 就是 void* 的 typedef void PyThread_release_lock(PyThread_type_lock lock) { pthread_lock *thelock = (pthread_lock *)lock; int status, error = 0; // dprintf 一個(gè)宏定義 都是打印消息的,不需要關(guān)心,而且默認(rèn)是不打印 dprintf(("PyThread_release_lock(%p) called\n", lock)); // 上鎖 status = pthread_mutex_lock( &thelock->mut ); CHECK_STATUS("pthread_mutex_lock[3]"); // 釋放全局解釋器鎖 thelock->locked = 0; // 解鎖 status = pthread_mutex_unlock( &thelock->mut ); CHECK_STATUS("pthread_mutex_unlock[3]"); // 因?yàn)獒尫帕巳纸忉屍麈i,現(xiàn)在需要喚醒一個(gè)被阻塞的線程 /* wake up someone (anyone, if any) waiting on the lock */ status = pthread_cond_signal( &thelock->lock_released ); CHECK_STATUS("pthread_cond_signal"); } // waitflag 表示如果沒(méi)有獲取鎖是否需要等待,如果不為 0 就表示沒(méi)獲取鎖就等待,即線程被掛起 int PyThread_acquire_lock(PyThread_type_lock lock, int waitflag) { int success; pthread_lock *thelock = (pthread_lock *)lock; int status, error = 0; dprintf(("PyThread_acquire_lock(%p, %d) called\n", lock, waitflag)); status = pthread_mutex_lock( &thelock->mut ); CHECK_STATUS("pthread_mutex_lock[1]"); success = thelock->locked == 0; // 如果沒(méi)有上鎖,則獲取鎖成功,并且上鎖 if (success) thelock->locked = 1; status = pthread_mutex_unlock( &thelock->mut ); CHECK_STATUS("pthread_mutex_unlock[1]"); if ( !success && waitflag ) { /* continue trying until we get the lock */ /* mut must be locked by me -- part of the condition * protocol */ status = pthread_mutex_lock( &thelock->mut ); CHECK_STATUS("pthread_mutex_lock[2]"); // 如果現(xiàn)在已經(jīng)有線程獲取到鎖了,就將當(dāng)前線程掛起 while ( thelock->locked ) { status = pthread_cond_wait(&thelock->lock_released, &thelock->mut); CHECK_STATUS("pthread_cond_wait"); } // 當(dāng)線程被喚醒之后,就說(shuō)明線程只有當(dāng)前線程在運(yùn)行可以直接獲取鎖 thelock->locked = 1; status = pthread_mutex_unlock( &thelock->mut ); CHECK_STATUS("pthread_mutex_unlock[2]"); success = 1; } if (error) success = 0; dprintf(("PyThread_acquire_lock(%p, %d) -> %d\n", lock, waitflag, success)); return success; }
pthread_lock 的結(jié)構(gòu)體如下所示:
其中鎖的結(jié)構(gòu)體如下所示:
typedef struct { char locked; /* 0=unlocked, 1=locked */ /* a <cond, mutex> pair to handle an acquire of a locked lock */ pthread_cond_t lock_released; pthread_mutex_t mut; } pthread_lock;
熟悉 pthread 編程的話,上面的代碼應(yīng)該很輕易可以看懂,我們現(xiàn)在來(lái)分析一下這個(gè)數(shù)據(jù)結(jié)構(gòu):
locked,表示全局解釋器鎖 GIL 是否有線程獲得鎖,0 表示沒(méi)有,1 則表示目前有線程獲取到了這把鎖。
lock_released,主要是用于線程的阻塞和喚醒的,如果當(dāng)前有線程獲取到全局解釋器鎖了,也就是 locked 的值等于 1,就將線程阻塞(執(zhí)行pthread_cond_wait),當(dāng)線程執(zhí)行釋放鎖的代碼 (PyThread_release_lock) 的時(shí)候就會(huì)將這個(gè)被阻塞的線程喚醒(執(zhí)行 pthread_cond_signal )。
mut,這個(gè)主要是進(jìn)行臨界區(qū)保護(hù)的,因?yàn)閷?duì)于 locked 這個(gè)變量的訪問(wèn)是線程不安全的,因此需要用鎖進(jìn)行保護(hù)。
在上面的代碼當(dāng)中我們?cè)敿?xì)介紹了 GIL 的實(shí)現(xiàn)源代碼,但是還沒(méi)有介紹虛擬機(jī)是如何使用它的。虛擬機(jī)在使用 GIL 的時(shí)候會(huì)有一個(gè)問(wèn)題,那就是如果多個(gè)線程同時(shí)在虛擬機(jī)當(dāng)中跑的時(shí)候,一個(gè)線程獲取到鎖了之后如果一直執(zhí)行的話,那么其他線程不久饑餓了嗎?因此虛擬機(jī)需要有一種機(jī)制保證當(dāng)有多個(gè)線程同時(shí)獲取鎖的時(shí)候不會(huì)讓線程饑餓。
在 CPython 當(dāng)中為了不讓線程饑餓有一個(gè)機(jī)制,就是虛擬機(jī)會(huì)有一個(gè) _Py_Ticker 記錄當(dāng)前線程執(zhí)行的字節(jié)碼的個(gè)數(shù),讓執(zhí)行的字節(jié)碼個(gè)數(shù)超過(guò) _Py_CheckInterval (虛擬機(jī)這只這個(gè)值為 100) 的時(shí)候就會(huì)釋放鎖,然后重新獲取鎖,在這釋放和獲取之間就能夠讓其他線程有機(jī)會(huì)獲得鎖從而進(jìn)行字節(jié)碼的執(zhí)行過(guò)程。相關(guān)的源代碼如下所示:
if (--_Py_Ticker < 0) { // 每執(zhí)行完一個(gè)字節(jié)碼就進(jìn)行 -- 操作,這個(gè)值初始化為 _Py_CheckInterval if (*next_instr == SETUP_FINALLY) { /* Make the last opcode before a try: finally: block uninterruptible. */ goto fast_next_opcode; } _Py_Ticker = _Py_CheckInterval; // 重新將這個(gè)值設(shè)置成 100 tstate->tick_counter++; #ifdef WITH_TSC ticked = 1; #endif // 這個(gè)主要是處理異常信號(hào)的 不用管 if (pendingcalls_to_do) { if (Py_MakePendingCalls() < 0) { why = WHY_EXCEPTION; goto on_error; } if (pendingcalls_to_do) /* MakePendingCalls() didn't succeed. Force early re-execution of this "periodic" code, possibly after a thread switch */ _Py_Ticker = 0; } #ifdef WITH_THREAD // 如果有 GIL 存在 if (interpreter_lock) { /* Give another thread a chance */ if (PyThreadState_Swap(NULL) != tstate) Py_FatalError("ceval: tstate mix-up"); PyThread_release_lock(interpreter_lock); // 首先釋放鎖 /* 其他線程的代碼在這就能夠運(yùn)行了 */ /* Other threads may run now */ // 然后獲取鎖 PyThread_acquire_lock(interpreter_lock, 1); if (PyThreadState_Swap(tstate) != NULL) Py_FatalError("ceval: orphan tstate"); } #endif }
GIL 的掙扎
在上面的內(nèi)容當(dāng)中我們?cè)敿?xì)講述了 GIL 的原理,我們可以很明顯的發(fā)現(xiàn)其中的問(wèn)題,就是一個(gè)時(shí)刻只有一個(gè)線程在運(yùn)行,限制了整個(gè)虛擬機(jī)的性能,但是整個(gè)虛擬機(jī)還有一個(gè)地方可以極大的提高整個(gè)虛擬機(jī)的性能,就是在進(jìn)行 IO 操作的時(shí)候首先釋放 GIL,然后在 IO 操作完成之后重新獲取 GIL,這個(gè) IO 操作是廣義上的 IO 操作,也包括網(wǎng)絡(luò)相關(guān)的 API,只要和設(shè)備進(jìn)行交互就可以釋放 GIL,然后操作執(zhí)行完成之后重新獲取 GIL。
在虛擬機(jī)的自帶的標(biāo)準(zhǔn)庫(kù)模塊當(dāng)中,就有很多地方使用了這種方法,比如文件的讀寫(xiě)和關(guān)閉,我們以文件關(guān)閉為例看一下 CPython 是如何操作的:
static int internal_close(fileio *self) { int err = 0; int save_errno = 0; if (self->fd >= 0) { int fd = self->fd; self->fd = -1; /* fd is accessible and someone else may have closed it */ if (_PyVerify_fd(fd)) { // 釋放全局解釋器鎖 這是一個(gè)宏 會(huì)調(diào)用前面的釋放鎖的函數(shù) Py_BEGIN_ALLOW_THREADS err = close(fd); if (err < 0) save_errno = errno; // 重新獲取全局解釋器鎖 也是一個(gè)宏 會(huì)調(diào)用前面的獲取鎖的函數(shù) Py_END_ALLOW_THREADS } else { save_errno = errno; err = -1; } } if (err < 0) { errno = save_errno; PyErr_SetFromErrno(PyExc_IOError); return -1; } return 0; }
這就會(huì)使得 Python 雖然有 GIL ,但是在 IO 密集型的程序上還是能打的,比如在網(wǎng)絡(luò)數(shù)據(jù)采集等領(lǐng)域, Python 還是有很大的比重。
總結(jié)
在本篇文章當(dāng)中詳細(xì)介紹了 CPython 選擇 GIL 的原因,以及 GIL 對(duì)于 Python 程序和虛擬機(jī)的影響,最后詳細(xì)分析了一個(gè)早期版本的 GIL 源代碼實(shí)現(xiàn)。GIL 可以很大程度上簡(jiǎn)化虛擬機(jī)的設(shè)計(jì)與實(shí)現(xiàn),因?yàn)橛幸话讶宙i,整個(gè)虛擬機(jī)的開(kāi)發(fā)就會(huì)變得更加簡(jiǎn)單,這種簡(jiǎn)單對(duì)于大型項(xiàng)目來(lái)說(shuō)是非常重要的。同時(shí)這對(duì) CPython 第三方庫(kù)的開(kāi)發(fā)者來(lái)說(shuō)也是福音。最后討論了 CPython 當(dāng)中 GIL 的實(shí)現(xiàn)和使用方式以及 CPython 使用 ticker 來(lái)保證線程不會(huì)饑餓的問(wèn)題。
以上就是深入理解python虛擬機(jī)GIL詳解的詳細(xì)內(nèi)容,更多關(guān)于python虛擬機(jī)GIL的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
基于opencv實(shí)現(xiàn)簡(jiǎn)單畫(huà)板功能
這篇文章主要為大家詳細(xì)介紹了基于opencv實(shí)現(xiàn)簡(jiǎn)單畫(huà)板功能,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2020-08-08淺談python中的@以及@在tensorflow中的作用說(shuō)明
這篇文章主要介紹了淺談python中的@以及@在tensorflow中的作用說(shuō)明,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2021-03-03python中查找excel某一列的重復(fù)數(shù)據(jù) 剔除之后打印
python查找excel某一列的重復(fù)數(shù)據(jù),剔除之后打印,供大家學(xué)習(xí)參考2013-02-02python實(shí)現(xiàn)多進(jìn)程并發(fā)控制Semaphore與互斥鎖LOCK
本文主要介紹了python實(shí)現(xiàn)多進(jìn)程并發(fā)控制Semaphore與互斥鎖LOCK,通過(guò)實(shí)例來(lái)介紹互斥鎖和進(jìn)程并發(fā)控制 semaphore的具體使用,感興趣的同學(xué)可以了解一下2021-05-05python獲取外網(wǎng)ip地址的方法總結(jié)
這篇文章主要介紹了python獲取外網(wǎng)ip地址的方法,實(shí)例總結(jié)了四種常用的獲取外網(wǎng)IP地址的技巧,需要的朋友可以參考下2015-07-07python多進(jìn)程執(zhí)行方法apply_async使用說(shuō)明
這篇文章主要介紹了python多進(jìn)程執(zhí)行方法apply_async使用說(shuō)明,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2021-03-03