欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

深入理解python虛擬機(jī)GIL詳解

 更新時(shí)間:2023年10月16日 08:22:48   作者:一無(wú)是處的研究僧  
在目前的 CPython 當(dāng)中一直有一個(gè)臭名昭著的問(wèn)題就是 GIL (Global Interpreter Lock ),就是全局解釋器鎖,他限制了 Python 在多核架構(gòu)當(dāng)中的性能,在本篇文章當(dāng)中我們將詳細(xì)分析一下 GIL 的利弊和 GIL 的 C 的源代碼

選擇 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)文章

最新評(píng)論