今天要跟你們一塊兒來學習一下Python的多線程機制。有兩個緣由,其一是本身在學習中常常會使用到多線程,其二固然是本身對Python中的多線程並非很瞭解。那麼,今天和你們一塊兒瞭解下~python
Python多線程機制 bootstrap
開發多線程的應用系統,是在平常開發中常常會遇到的需求。同時,Python也爲多線程系統的開發提供了很好的支持。
你們應該都知道,Python多線程機制是在GIL(Global Interpreter Lock)全局解釋鎖的基礎上創建的。windows
那麼Python爲何須要全局解釋鎖?數據結構
爲何須要全局解釋鎖?多線程
咱們知道,要支持多線程的話,一個基本的要求就是不一樣線程對共享資源訪問的互斥,因此Python中引入了GIL,固然這是第一個緣由。ide
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中的多線程,今天咱們就學到這裏。