對Python應用線程介紹說明
在Python中提供的一些接口中,一定不能少的肯定是創建Python應用線程的接口,倘若沒有這個接口,編程還有什么太多的意義啊,對多線程的支持并非是沒有代價的。
我們注意到boot->interp中保存了Python的PyInter- preterState對象,這個對象中攜帶了Python的module pool這樣的全局信息,Python中所有的thread都會共享這些全局信息。關于代碼清單所示的多線程環境的初始化動作。
有一點需要特別說明,當Python啟動時,是并不支持多線程的。換句話說,Python中支持多線程的數據結構以及GIL都是沒有創建的,Python之所以有這種行為是因為大多數的Python應用線程都不需要多線程的支持。
假如一個簡單地統計詞頻的Python腳本中居然出現了多線程,面對這樣的代碼,我們一定都會抓狂的J。對多線程的支持并非是沒有代價的。最簡單的一點,如果激活多線程機制,而執行的Python程序中并沒有多線程,那么在100條指令之后,Python虛擬機同樣會激活線程的調度。
而如果不激活多線程,Python虛擬機則不用做這些無用功。所以Python選擇了讓用戶激活多線程機制的策略。在Python虛擬機啟動時,多線程機制并沒有被激活,它只支持單線程,一旦用戶調用thread.start_new_thread。
明確指示Python虛擬機創建新的線程,Python就能意識到用戶需要多線程的支持,這個時候,Python虛擬機會自動建立多線程機制需要的數據結構、環境以及那個至關重要的GIL。
在這里,我們終于看到了Python中多線程機制的平臺相關性,在Python25\Python目錄下,有一大批thread_***.h這樣的文件。在這些文件中,包裝了不同操作系統的原生線程,并通過統一的接口暴露給Python,比如這里的PyThread_allocate_lock就是這樣一個接口。
我們這里的thread_nt.h中包裝的是Win32平臺的原生thread,在本章中后面的代碼剖析中,還會有大量與平臺相關的代碼,我們都以Win32平臺為例。在PyThread_allocate_lock中,與PyEval_InitThreads非常類似的,它會檢查一個initialized的變量,如果說GIL指示著Python的多線程環境是否已經建立。
那么這個initialized變量就指示著為了使用底層平臺所提供的原生thread,必須的初始化動作是否完成。這些必須的初始化動作通常都是底層操作系統所提供的API,不同的操作系統可能需要不同的初始化動作。
一切真相大白了,原來,GIL(NRMUTEX)中的hevent就是Win32平臺下的Event這個內核對象,而其中的thread_id將記錄任一時刻獲得GIL的線程的id。到了這里,Python中的線程互斥機制的真相漸漸浮出水面。
看來Python應用線程是通過Win32下的Event來實現了線程的互斥,熟悉Win32的朋友馬上就可能想到,與這個Event對應的,必定有一個WaitForSingleObject。在PyEval_InitThreads通過PyThread_allocate_lock成功地創建了GIL之后,當前線程就開始遵循Python的多線程機制的規則:
在調用任何Python C API之前,必須首先獲得GIL。因此PyEval_InitThreads緊接著通過PyThread_acquire_lock嘗試獲得GIL。最終,一個線程在釋放GIL時,會通過SetEvent通知所有在等待GIL的hevent這個Event內核對象的線程,結合前面的分析。
如果這時候有線程在等待GIL的hevent,那么將被操作系統喚醒。這就是我們在前面介紹的Python將線程調度的第二個難題委托給操作系統來實現的機制。到了這時,調用PyEval_InitThread的線程(也就是Python主線程)已經成功獲得了GIL,最后會調用PyThread_get_thread_ident()。
通過Win32的API:GetCurrent- ThreadId,獲得當前Python主線程的id,并將其賦給main_thread,main_thread是一個靜態全局變量。專職存儲Python主線程的線程id,用以判斷一個線程是否是Python主線程。最后,我們在給出整個PyEval_InitThread的函數調用關系。
值得注意的是,obj.done是一個Win32下的Semaphore內核對象,這個特殊的內核對象的用途我們馬上就會看到。我們創建線程的工作需要func和arg,但是Win32下創建線程的API只允許用戶指定一個自定義的參數,這就是需要用obj來打包的原因。
完成打包之后,調用Win32下創建thread的API:_beginthread來完成線程的創建。奇怪的是,我們期望的線程過程應該是thread1.py中定義的那個threadPoc呀,而這里指定的線程過程卻是一個相當面生的bootstrap。實際上,在bootstrap中,會最終調用thread1.py中定義的threadProc。
但是,這里有一個至關重要的轉折,還記得我們現在在哪里嗎?沒錯,我們現在是沿著主線程的執行路徑在剖析,而對bootstrap的調用并不是在主線程中發生的,而是在通過_beginthread所創建的子線程中發生的。從這里開始,我們需要特別注意代碼的執行是在哪個線程中執行的,這對于理解Python應用線程機制相當重要。
好了,花開兩朵,各表一枝。我們繼續沿著主線程的執行路徑前進。如果不出什么意外,_beginthread將最終成功地創建Win32下的原生線程,并順利返回。在返回之后,主線程開始將自己掛起,等待obj.done。
我們前面看到,這是一個Win32的Semaphore內核對象。由于obj已經作為參數傳遞給了子線程,所以我們猜想,子線程會設置這個Semaphore,并最終喚醒主線程。現在我們來理清一下Python當前的狀態。
Python當前實際上由兩個Win32下的原生thread構成,一個是執行python程序(python.exe)時操作系統創建的主線程,另一個是我們通過thread1.py創建的子線程。主線程在執行PyEval_InitThread的過程中。
得了GIL,但是目前已經被掛起,這是為了等待子線程中控制著的obj.done。子線程的線程過程是bootstrap,不過我們剛才已經猜測了,從bootstrap出發,最終將在Python解釋器中執行python1.py中定義的theadProc。但是,我們知道,子線程為了訪問Python解釋器,必須首先獲得GIL,這是Python世界的游戲規則,誰也不能例外。
【編輯推薦】