Python多線程機制


今天要跟大家一起來學習一下Python的多線程機制。有兩個原因,其一是自己在學習中經常會使用到多線程,其二當然是自己對Python中的多線程並不是很了解。那么,今天和大家一起了解下~

Python多線程機制 

開發多線程的應用系統,是在日常開發中經常會遇到的需求。同時,Python也為多線程系統的開發提供了很好的支持。
大家應該都知道,Python多線程機制是在GIL(Global Interpreter Lock)全局解釋鎖的基礎上建立的。

那么Python為什么需要全局解釋鎖?

為什么需要全局解釋鎖?

我們知道,要支持多線程的話,一個基本的要求就是不同線程對共享資源訪問的互斥,所以Python中引入了GIL,當然這是第一個原因。

Python中的GIL是一個非常霸道的互斥實現,在一個線程擁有了解釋器的訪問權之后,其它的所有線程都必須等待它釋放解釋器的訪問權,即使這些線程的下一條指令並不會互相影響。

這樣的說法也就意味着,無論如何,在同一時間,只能有一個線程能訪問Python提供的API。因為單處理器的本質是不可能並行的,這里的同一時間確實對於單處理器是毫無意義的,但是對於多處理器,同一時間,確實可以有多個時間獨立運行。然而正是由於GIL限制了這樣的情形,使得多處理器最終退化為單處理器,性能大打折扣。那么,為什么還要使用GIL呢?這里就要提到第二個原因。

當然,Python社區也早都認識到了這個問題,並且在不斷探索,Greg Stein和Mark Hammond兩位老兄曾經創建過一份去除GIL的branch,但是很不幸,這個分支在很多的基准測試中,尤其是在單線程的測試上,效率只有使用GIL的一半左右。

使用GIL時,保護機制的粒度比較大,也就是我們似乎只需要將可能被多個線程共享的資源保護起來即可,對於不會被多個線程共享的資源,完全可以不用保護。但是,如果使用更細粒度的鎖機制進行保護,那么,會導致大量的加鎖和解鎖功能,加鎖和解鎖對於操作系統來說,是一個比較重量級的動作,同時,沒有GIL的保護,編寫Python的擴展模塊的難度也大大增加。

所以,目前為止,GIL仍然是多線程機制的基石。

對於Python而言,字節碼解釋器是Python的核心所在,所以Python通過GIL來互斥不同線程對解釋器的使用。這里舉個例子進行說明:

假設,現在有三個線程A、B和C,它們都需要解釋器來執行字節碼,進行對應的計算,那么在這之前,它們必須獲得GIL。那么現在假設線程A獲得了GIL,其它線程只能等A釋放GIL之后,才能獲得。

對!是這樣沒錯,於是,有兩個問題:

1. 線程A何時釋放GIL呢(如果A使用完解釋器之后才釋放GIL,那么,並行的計算退化為串行,多線程的意義何在?)

2. 線程B和C誰將在A釋放GIL之后獲得GIL呢?

所以毫無疑問的,Python擁有其自己的一套線程調度機制。

關於線程調度

和操作系統的進程調度一樣,線程調度機制主要解決兩個問題:

1. 在何時掛起當前線程,選擇處於等待狀態的下一個線程?

2. 在眾多處於等待狀態的線程中,應該選擇激活哪個線程?

對於何時進行線程調度的問題,是由Python自身決定的。我們可以聯想操作系統進行進程切換的問題,當一個進程執行了一段時間之后,發生了時鍾中斷,於是操作系統響應時鍾中斷,並在這時開始進程的調度。

與此類似,Python中通過軟件模擬了這樣的中斷,來激活線程的調度。Python的字節碼解釋器是按照指令的順序一條一條的順序執行從而工作的,Python內部維護着這樣一個數值,作為Python內部的時鍾,假設這個值為N,那么Python將在執行了N條指令之后立刻啟動線程調度機制。

也就是說,當一個線程獲得GIL后,Python內部的監測機制就開始啟動,當這個線程執行了N條指令后,Python解釋器將強制掛起當前線程,開始切換到下一個處於等待狀態的線程。

在Python中,可以這樣獲得這個數值(N):


那么,下一個問題,Python會在眾多等待的線程中選擇哪一個呢?

答案是,不知道。因為這個問題是交給了底層的操作系統來解決的,Python借用了底層操作系統所提供的線程調度機制來決定下一個獲得GIL進入解釋器的線程是誰。

所以說,Python中的線程實際上就是操作系統所支持的原生線程。

那么,接下來,我們一起揭開Python中GIL的真實面目。

關於GIL

應該知道,Python中多線程常用的兩個模塊:Thread和在其之上的threading。其中Thread是使用C實現的,而Threading是用python實現。

我們可以通過Thread模塊進行分析(以Python2.7.13為例)。

創建線程
首先從創建線程說起,在threadmodule.c中,thread_PyThread_start_new_thread()函數通過三個主要的動作完成一個線程的創建:

//創建bootstate結構
boot = PyMem_NEW(struct bootstate, 1); if (boot == NULL) return PyErr_NoMemory(); boot->interp = PyThreadState_GET()->interp; boot->func = func; boot->args = args; boot->keyw = keyw; boot->tstate = _PyThreadState_Prealloc(boot->interp); if (boot->tstate == NULL) { PyMem_DEL(boot); return PyErr_NoMemory(); } Py_INCREF(func); Py_INCREF(args); Py_XINCREF(keyw); // 初始化多線程環境
PyEval_InitThreads(); //創建線程
ident = PyThread_start_new_thread(t_bootstrap, (void*) boot); if (ident == -1) { PyErr_SetString(ThreadError, "can't start new thread"); Py_DECREF(func); Py_DECREF(args); Py_XDECREF(keyw); PyThreadState_Clear(boot->tstate); PyMem_DEL(boot); return NULL; } return PyInt_FromLong(ident);

1. 創建並初始化bootstate結構boot,在boot中,將保存關於Python的一切信息(線程過程,線程過程參數等)。

2. 初始化Python的多線程環境。

3. 以boot為參數,創建操作系統的原生線程。

從以上代碼可以看出,Python在剛啟動時,並不支持多線程,也就是說,Python中支持多線程的數據結構以及GIL都是沒有創建的。當然這是因為大多數的Python程序都不需要Python的支持。

在Python虛擬機啟動時,多線程機制並沒有被激活,它只支持單線程,一旦用戶調用thread.start_new_thread,明確的告訴Python虛擬機需要創建新的線程,這時Python意識到用戶需要多線程的支持,這個時候,Python虛擬機會自動建立多線程需要的數據結構、環境以及GIL。

建立多線程環境

建立多線程環境,主要就是創建GIL。那么GIL是如何實現的呢?
打開"python/ceval.c":

static PyThread_type_lock interpreter_lock = 0; /* This is the GIL */
static PyThread_type_lock pending_lock = 0; /* for pending calls */
static long main_thread = 0; int PyEval_ThreadsInitialized(void) { return interpreter_lock != 0; } void PyEval_InitThreads(void) { if (interpreter_lock) return; interpreter_lock = PyThread_allocate_lock(); PyThread_acquire_lock(interpreter_lock, 1); main_thread = PyThread_get_thread_ident(); }

在這段代碼中,iterpreter_lock就是GIL。

無論創建多少個線程,Python建立多線程環境的動作只會執行一次。在創建GIL之前,Python會檢查GIL是否已經被創建,如果是,則不再進行任何動作,否則,就會去創建這個GIL。

在上述代碼中,我們可以看到,創建GIL使用的是Pythread_allocate_lock完成的,下面看看該函數的內部實現:

PyThread_type_lock PyThread_allocate_lock(void) { PNRMUTEX aLock; dprintf(("PyThread_allocate_lock called\n")); if (!initialized) PyThread_init_thread(); aLock = AllocNonRecursiveMutex() ; dprintf(("%ld: PyThread_allocate_lock() -> %p\n", PyThread_get_thread_ident(), aLock)); return (PyThread_type_lock) aLock; }

可以看到該函數返回了alock,alock是結構體PNRMUTEX,實際上就是我們需要創建的那個interperter_lock(GIL)。這么說來,GIL就是結構體PNRMUTEX呀,於是我們找來它的真身:

typedef struct NRMUTEX { LONG owned ; DWORD thread_id ; HANDLE hevent ; } NRMUTEX, *PNRMUTEX ;

這里又三個變量,owned、thread_id和hevent。這里的hevent是windows平台下的Event這個內核對象,也就是通過Event來實現線程之間的互斥。thread_id將記錄任一時刻獲得GIL的線程的id。

那么owned是什么呢?

GIL中的owned是指示GIL是否可用的變量,它的值被初始化為-1,Python會檢查這個值是否為1,如果是,則意味着GIL可用,必須將其置為0,當owned為0后,表示該GIL已經被一個線程占用,不可再用;同時,當一個線程開始等待GIL時,其owned就會被增加1;當一個線程最終釋放GIL時,一定會將GIL的owned減1,這樣,當所有需要GIL的線程都最終釋放了GIL之后,owned將再次變為-1,意味着GIL再次變為可用。

 

關於Python中的多線程,今天我們就學到這里。

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM