今天要跟大家一起來學習一下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中的多線程,今天我們就學到這里。