詳細說明Python虛擬機狀態(tài)問題
現在說明一下關于Python虛擬機的狀態(tài)問題,實際上Python當前由兩個原生thread構成,一個是主線程執(zhí)行程序(python.exe)時操作系統(tǒng)創(chuàng)建的,另一個是通過thread1.py創(chuàng)建的子線程。
在代碼清單15-1的[1]中,我們注意到boot->interp中保存了Python虛擬機的PyInter- preterState對象,這個對象中攜帶了Python的module pool這樣的全局信息,Python中所有的thread都會共享這些全局信息。
關于代碼清單15-1的[2]處所示的多線程環(huán)境的初始化動作,有一點需要特別說明,當Python啟動時,是并不支持多線程的。換句話說,Python中支持多線程的數據結構以及GIL都是沒有創(chuàng)建的,Python之所以有這種行為是因為大多數的Python程序都不需要多線程的支持。
- [threadmodule.c]
- static PyObject* thread_PyThread_start_new_thread(PyObject *self, PyObject
- *fargs)
- {
- PyObject *func, *args, *keyw = NULL;
- struct bootstate *boot;
- long ident;
- PyArg_UnpackTuple(fargs, "start_new_thread", 2, 3, &func, &args, &keyw);
- //[1]:創(chuàng)建bootstate結構
- boot = PyMem_NEW(struct bootstate, 1);
- boot->interp = PyThreadState_GET()->interp;
- boot->funcfunc = func;
- boot->argsargs = args;
- boot->keywkeyw = keyw;
- //[2]:初始化多線程環(huán)境
- PyEval_InitThreads(); /* Start the interpreter's thread-awareness */
- //[3]:創(chuàng)建線程
- ident = PyThread_start_new_thread(t_bootstrap, (void*) boot);
- return PyInt_FromLong(ident);
- [thread.c]
- /* Support for runtime thread stack size tuning.
- A value of 0 means using the platform's default stack size
- or the size specified by the THREAD_STACK_SIZE macro. */
- static size_t _pythread_stacksize = 0;
- [thread_nt.h]
- long PyThread_start_new_thread(void (*func)(void *), void *arg)
- {
- unsigned long rv;
- callobj obj;
- obj.id = -1; /* guilty until proved innocent */
- obj.func = func;
- obj.arg = arg;
- obj.done = CreateSemaphore(NULL, 0, 1, NULL);
- rv = _beginthread(bootstrap, _pythread_stacksize, &obj); /* use default stack size */
- if (rv == (unsigned long)-1) {
- //創(chuàng)建raw thread失敗
- obj.id = -1;
- }
- else {
- WaitForSingleObject(obj.done, INFINITE);
- }
- CloseHandle((HANDLE)obj.done);
- return obj.id;
- }
假如一個簡單地統(tǒng)計詞頻的Python腳本中居然出現了多線程,面對這樣的代碼,我們一定都會抓狂的J。對多線程的支持并非是沒有代價的,最簡單的一點,如果激活多線程機制。
而執(zhí)行的Python程序中并沒有多線程,那么在100條指令之后,Python虛擬機同樣會激活線程的調度。而如果不激活多線程,Python虛擬機則不用做這些無用功。所以Python選擇了讓用戶激活多線程機制的策略。在Python虛擬機啟動時,多線程機制并沒有被激活,它只支持單線程,一旦用戶調用thread.start_new_thread。
明確指示Python虛擬機創(chuàng)建新的線程,Python就能意識到用戶需要多線程的支持,這個時候,Python虛擬機會自動建立多線程機制需要的數據結構、環(huán)境以及那個至關重要的GIL。在這里,我們終于看到了Python中多線程機制的平臺相關性,在Python25\Python目錄下,有一大thread_***.h這樣的文件,在這些文件中,包裝了不同操作系統(tǒng)的原生線程。#t#
并通過統(tǒng)一的接口暴露給Python,比如這里的PyThread_allocate_lock就是這樣一個接口。我們這里的thread_nt.h中包裝的是Win32平臺的原生thread,在本章中后面的代碼剖析中,還會有大量與平臺相關的代碼。
我們都以Win32平臺為例。一切真相大白了,原來,GIL(NRMUTEX)中的hevent就是Win32平臺下的Event這個內核對象,而其中的thread_id將記錄任一時刻獲得GIL的線程的id。
到了這里,Python虛擬機中的線程互斥機制的真相漸漸浮出水面,看來Python是通過Win32下的Event來實現了線程的互斥,熟悉Win32的朋友馬上就可能想到,與這個Event對應的,必定有一個WaitForSingleObject。
在PyEval_InitThreads通過PyThread_allocate_lock成功地創(chuàng)建了GIL之后,當前線程就開始遵循Python的多線程機制的規(guī)則:在調用任何Python C API之前,必須首先獲得GIL。因此PyEval_InitThreads緊接著通過PyThread_acquire_lock嘗試獲得GIL。
與InterlockedCompareExchange相同的,InterlockedIncrement也是一個原子操作,其功能是將mutex->owned的值增加1。從這里可以看到,當一個線程開始等待GIL時,其owned就會被增加1。顯然我們可以猜測,當一個線程最終釋放GIL時,一定會將GIL的owned減1,這樣當所有需要GIL的線程都最終釋放了GIL之后,owned會再次變?yōu)?1,意味著GIL再次變?yōu)榭捎谩?/P>
為了清晰地展示這一點,我們現在就來看看PyThread_aquire_lock的逆運算,PyThread_release_lock每一個將從運行轉態(tài)轉為等待狀態(tài)的線程都會在被掛起之前調用它以釋放對GIL的占有。
【編輯推薦】