一,線程
與前面介紹的進程一樣,線程也有兩部分組成:
1)一個線程核心對象,作業系統用它來管理線程。核心對象中還儲存了線程的各種統計資訊,包括掛起計數、結束代碼等,以便於系統對線程的管理。核心對象中有一個CONTEXT結構,這個結構中儲存了線程上一次執行的時候CPU寄存器的狀態。
2)一個線程棧,用於維護線程執行時所需的所有函數參量和局部變數。
位於同一個進程的線程共用進程的地址空間且它們共用進程控制代碼表。因為控制代碼表是針對進程的。進程需要很多的系統資源,而線程僅僅需要一個線程核心對象和線程棧就可以了,因此線程比進程的開銷要小得多。採用多線程來處理問題也是理所當然的了。
採用多線程可以提高程式的執行效率,但是多線程也存在很多問題。在嘗試使用多線程時如果處理不當還可能會引入新的問題。如同步問題。
二,編寫第一個線程函數
每個線程都需要一個進入點函數。這是線程執行的起點。主線程的進入點函數是_tmain或_tWinmain。如果在進程中建立新線程必須提供自己的進入點函數。
形如:
DWORD WINAPI ThreadFunc(PVOID pvParam){
DWORD dwResult = 0;
...
return(dwResult);
}
線程函數可以是任何我們希望它執行的任務,最終線程函數會終止並返回。類似於進程核心對象,如果線程核心對象使用計數變為0,則會被銷毀。
1) 預設情況下主線程的進入點函數必須命名為main,wmain,WinMain或wWinMain。我們可以通過設定/ENTRY:連結器選項來指定另一個函數作為進入點函數。
2)主線程進入點函數有字串參數,所以它提供了ANSI/Unicode版本。相反,線程函數只有一個參數,其意義可由我們定義。可以為其傳遞一個值,也可以將其作為某個資料結構的指標。這需要線上程函數內部做類型轉換。
3)線程函數必須返回一個值,它的值傳遞給ExitThread,作為線程的結束代碼。
4)線程函數儘可能地使用局部變數或函數參數,它們是線上程棧上建立的。不太可能被其他線程破壞。使用靜態變數或全域變數時其他線程可以訪問這些變數,這會導致同步和互斥問題。
三,CreateThread 函數
如果想建立一個或多個輔助線程,只需讓一個正在啟動並執行線程調用CreateThread :
HANDLE CreateThread(
PSECURITY_ATTRIBUTES psa, //指向SECURITY_ATTRIBUTES結構的指標;
DWORD cbStackSize, //制定線程可以為其線程棧使用多少地址空間; 棧空間大小 ,傳入0則預設使用內部值 連結器/STACK 可以改變大小 預設1M
PTHREAD_START_ROUTINE pfnStartAddr, //線程函數的地址;
PVOID pvParam, //線程函數參數
DWORD dwCreateFlags, //指定額外的標誌來控制線程的建立; CREATE_SUSPENDED 暫停執行
PDWORD pdwThreadID); //儲存系統分配給新線程的ID;
例:
DWORD WINAPI FirstThread(PVOID pvParam)
{
int x=0; //這裡的函數,為SecoundThread 的傳回值,最好聲明為static,防止FirstThread 執行完,SecoundThread 返回時,訪問棧越界。
DWORD dwThreadID;
HANDLE hThread = CreatThread( NULL, 0 , SecondThread , (PVOID)&x , 0, &dwThread);
}
DWORD WINAPI SecondThread (PVOID pvParam)
{
*(int *)pvParam =5;
}
調用 CreateThread 時,系統會建立一個線程核心對象。系統從進程地址空間中分配記憶體給線程棧使用。新線程可以訪問進程核心對象的所有控制代碼、進程中的所有記憶體以及同一個進程中其它所有線程的棧。
CreateThread 函數時用於建立線程的Windows函數。不過如果寫的是C/C++代碼,就絕對不要調用 CreateThread。 正確地選擇是使用Microsoft C++運行庫函數_beginthreadex 。如果使用的不是Microsoft C++編譯器,你的編譯器的供應商應該提供類似的函數來替代 CreateThread 。不管這個替代函數時什麼,都必須使用它。
四,終止運行線程
線程可以通過以下4種方法來終止運行:
線程函數返回(這是強烈推薦的);
線程通過ExitThread 函數“殺死”自己(應避免);
同一個進程或另一個進程中的線程調用TerminateThread 函數(應避免);
包含線程的進程終止運行(應避免);
五,線程函數返回
設計線程函數時,應該確保在我們希望線程終止運行時,就讓它們返回。這是保證線程的所有資源被正確清理的唯一方式。讓線程函數返回,可以確保一下正確地應用程式權利工作都得以執行:
線程函數中建立的所有C++對象都通過其解構函式被正確銷毀;
作業系統正確釋放線程棧使用的記憶體;
作業系統把線程的結束代碼(線上程的核心對象中維護)設為線程函數的傳回值;
系統減少線程的核心對象的使用計數;
六,ExitThread函數
VOID ExitThread(DWORD dwExitCode);
該函數將終止線程的執行,並導致作業系統清理該線程使用的所有系統資源。但是你的C/C++資源(如C++類對象)不會被銷毀。所以更好的做法是直接從線程函數返回,不要自己調用 ExitThread 。
ExitThread 是Windows用於“殺死”線程的函數,如果要寫C/C++代碼,就絕對不要調用 ExitThread 。相反,應該使用C++運行庫函數_endthreadex 。 如果使用的不是Microsoft的C++編譯器,那麼編譯器供應商應該提供它們自己的 ExitThread 替代函數。 不管這個替代函數是什麼,都必須使用它。
七,TerminateThread函數
BOOL TerminateThread(
HANDLE hThread,
DWORD dwExitCode);
TerminateThread 是非同步函數 ,ExitThread 函數來終止線程,線程的堆棧會被銷毀,而 TerminateThread ,除非擁有此線程的進程終止運行,否則系統不會銷毀這個線程的堆棧。
八,線程終止運行時
一個線程終止時,系統會一次執行以下操作:
線程擁有的所有使用者物件控制代碼會被釋放;
線程的結束代碼從STILE_ACTIVE變為傳給ExitThread或TerminateThread函數的代碼;
線程核心對象的狀態變為已觸發狀態;
如果線程是進程中的最後一個活動線程,系統認為進程也終止了;
線程核心對象的使用計數遞減1;
九,線程內幕
CreateThread 函數的一個調用 導致 系統建立一個線程核心對象,該對象最初的使用計數為2。( 建立線程核心對象加1,返回線程核心物件控點加1 ),所以除非線程終止,而且 CreateThread 返回的控制代碼關閉,否則線程核心對象不會被銷毀。該線程對象的其它屬性也被初始化:暫停計數被設為1,結束代碼被裝置STILE_ACTIVE(0x103),而且對象被設為未觸發狀態。
建立了核心對象,系統就分配記憶體,供線程的堆棧使用。此記憶體是從進程的地址空間分配的,因為線程沒有自己的地址空間。系統將來個值寫入新線程堆棧的最上端,1所示,即調用的線程函數及其參數。
每個線程都有自己的一組CPU寄存器,稱為線程的上下文(context)。上下文反映了當線程上一次執行時,線程CPU寄存器的狀態。CONTEXT結構儲存線上程的核心對象中。
當線程核心對象被初始化的時候,CONTEXT結構的堆棧指標寄存器被設為pfnStartAddr線上程堆棧中的地址。而指令指標寄存器被設為RtlUserThreadStart函數的地址。
VOID RtlUserThreadStart(PTHREAD_START_ROUTINE pfnStartAddr, PVOID pvParam) {
__try {
ExitThread((pfnStartAddr)(pvParam));
}
__except(UnhandledExceptionFilter(GetExceptionInformation())) {
ExitProcess(GetExceptionCode());
}
// NOTE: We never get here.
}
線程完全初始化之後,系統檢查CREATE_SUSPENDED標誌是否已被傳給CreateThread函數。如果此標記沒有傳遞,系統將線程的掛起計數遞減至0;隨後,線程就可以調度給一個處理器去執行。然後,系統在實際的 CPU寄存器中載入上一次線上程上下文中儲存的值。現在,線程可以在其進程的地址空間中執行代碼並處理資料了。
新線程執行RtlUserThreadStart函數的時候,將發生以下事情:
圍繞線程函數,會設定一個結構化異常處理(SEH)幀。這樣一來,線程執行期間所產生的任何異常都能得到系統的預設處理。
系統調用線程函數,把傳給CreateThread函數的pvParam參數傳給它。
線程函數返回時,RtlUserThreadStart調用ExitThread,將你的線程函數的傳回值傳給它。線程核心對象的使用計數遞減,而後線程停止執行。
如果線程產生了一個未被處理的異常,RtlUserThreadStart函數所設定的SEH幀會處理這個異常。通常,這意味著系統會向使用者顯示一個訊息框,而且當使用者關閉此訊息框時,RtlUserThreadStart會調用ExitProcess來終止真箇進程,而不是終止有問題的線程。
當一個進程的主線程初始化時,其指令指標指向RtlUserThreadStart,當RtlUserThreadStart開始執行時,它會調用C/C++運行庫的啟動代碼,後者初始化繼而調用你的_tmain或_tWinMain函數。
十,C/C++運行庫注意事項
為了保證C和C++多線程應用程式正常運行,必須建立一個資料結構,並使之與使用了C/C++運行庫函數的每個線程關聯。然後,在調用C/C++運行庫函數時,那些函數必須知道去尋找主調線程的資料區塊,從而避免影響到其它線程
編寫C/C++應用程式,一定不要叫用作業系統的CreateThread函數,相反,應該調用C/C++運行庫函數_beginthreadex:
uintptr_t __cdecl _beginthreadex ( void *psa, unsigned cbStackSize, unsigned (__stdcall * pfnStartAddr) (void *), void * pvParam, unsigned dwCreateFlags, unsigned *pdwThreadID) { _ptiddata ptd; // Pointer to thread's data block uintptr_t thdl; // Thread's handle // Allocate data block for the new thread. if ((ptd = (_ptiddata)_calloc_crt(1, sizeof(struct _tiddata))) == NULL) goto error_return; // Initialize the data block. initptd(ptd); // Save the desired thread function and the parameter // we want it to get in the data block. ptd->_initaddr = (void *) pfnStartAddr; ptd->_initarg = pvParam; ptd->_thandle = (uintptr_t)(-1); // Create the new thread. thdl = (uintptr_t) CreateThread ((LPSECURITY_ATTRIBUTES)psa, cbStackSize, _threadstartex , (PVOID) ptd, dwCreateFlags, pdwThreadID); if (thdl == 0) { // Thread couldn't be created, cleanup and return failure. goto error_return; } // Thread created OK, return the handle as unsigned long. return(thdl); error_return: // Error: data block or thread couldn't be created. // GetLastError() is mapped into errno corresponding values // if something wrong happened in CreateThread. _free_crt(ptd); return((uintptr_t)0L); }
對於_beginthreadex函數有以下重點
1) 每個線程都有自己的專用_tiddata記憶體塊,它們是從C/C++運行庫的堆(heap)上分配的。
2)傳給_beginthreadex的線程函數的地址儲存在_tiddata記憶體塊中。
3)_beginthreadex確實會在內部調用CreateThread,因為作業系統只知道用這種方式來建立一個新線程。
4)CreateThread函數被調用時,傳給它的函數地址是_threadstartex(而非pfnStartAddr)。另外,參數地址是_tiddata結構的地址,而非pvParam。
5)如果一切順利,會返回線程的控制代碼,就像CreateThread那樣。任何操作失敗,會返回0。
為新線程初始化_tiddata結構之後,接著來看看這個結構如何與線程關聯的:
static unsigned long WINAPI _threadstartex (void* ptd) {
// Note: ptd is the address of this thread's tiddata block.
// Associate the tiddata block with this thread so
// _getptd() will be able to find it in _callthreadstartex.
TlsSetValue(__tlsindex, ptd);
// Save this thread ID in the _tiddata block.
((_ptiddata) ptd)->_tid = GetCurrentThreadId();
// Initialize floating-point support (code not shown).
// call helper function.
_callthreadstartex ();
// We never get here; the thread dies in _callthreadstartex.
return(0L);
}
static void _callthreadstartex(void) {
_ptiddata ptd; /* pointer to thread's _tiddata struct */
// get the pointer to thread data from TLS
ptd = _getptd();
// Wrap desired thread function in SEH frame to
// handle run-time errors and signal support.
__try {
// Call desired thread function, passing it the desired parameter.
// Pass thread's exit code value to _endthreadex.
_endthreadex (
((unsigned (WINAPI *)(void *))(((_ptiddata)ptd)->_initaddr))
(((_ptiddata)ptd)->_initarg)) ;
}
__except(_XcptFilter(GetExceptionCode(), GetExceptionInformation())){
// The C run-time's exception handler deals with run-time errors
// and signal support; we should never get it here.
_exit(GetExceptionCode());
}
}
關於_threadstartex函數,要注意一下幾大重點:
1)新的線程首先執行RtlUserThreadStart(在NTDLL.dll檔案中),然後再跳轉到_threadstartex;
2)_threadstartex唯一的參數就是新線程的_tiddata記憶體塊的地址;
3)TlsSetValue是一個作業系統函數,它將一個值與主調函數關聯起來。這就是所謂的線程局部儲存(Thread Local Storage,TLS)。_threadstartex函數將_tiddata記憶體塊與建立線程關聯起來;
4)在無參數的輔助函數_callthreadstartex中,有一個SEH幀,它將預期要執行的線程函數包圍起來。這個幀處理著與運行庫有關的許多事情—比如執行階段錯誤;
5)預期要執行的線程函數會被調用,並向其傳遞預期的參數。函數的地址和參數會被儲存在TLS的_tiddata資料區塊中,並會在_callthreadstartex中從TLS中擷取;
6)線程函數的傳回值被認為是線程的結束代碼;
再來看看_endthreadex:
void __cdecl _endthreadex (unsigned retcode) { _ptiddata ptd; // Pointer to thread's data block // Clean up floating-point support (code not shown). // Get the address of this thread's tiddata block. ptd = _getptd_noexit (); // Free the tiddata block. if (ptd != NULL) _freeptd(ptd); // Terminate the thread. ExitThread(retcode); }
對於_endthreadex函數,要注意一下幾點:
1)C運行庫的_getptd_noexit函數在內部叫用作業系統的TlsGetValue函數,後者擷取主調函數的tiddata記憶體塊的地址;
2)然後_endthreadex將此資料區塊釋放,並叫用作業系統的ExitThread函數來實際地銷毀線程。它會傳遞並正確設定結束代碼;
我們應該避免使用ExitThread函數,因為此函數會“殺死”主調線程,而且不允許它從當前執行的函數返回。由於函數沒有返回,所以構造的任何C++對象都不會被析構;它還會阻止線程的_tiddata記憶體塊被釋放,使應用程式出現記憶體泄露(直到整個進程終止)。
我們應該盡量用C/C++運行庫函數(_beginthreadex,_endthreadex)而盡量避免使用作業系統提供的函數(CreateThread,ExitThread)。
十一,擔心偽控制代碼
HANDLE GetCurrentProcess();
HANDLE GetCurrentThread();
這兩個函數返回到主調函數的進程核心對象或線程核心對象的一個偽控制代碼。它們不會再主調進程的控制代碼表中建立控制代碼。即返回的控制代碼並不在進程控制代碼表中有實際的表項,也不會影響進程核心對象或線程核心對象的使用計數。如果嗲用CloseHandle函數,並傳入一個“偽控制代碼”,CloseHandle只是簡單的忽略此調用。
將偽控制代碼轉換為真正的控制代碼,DuplicateHandle函數可以執行這個轉換,如在進程控制代碼表中建立線程核心控制代碼:
DuplicateHandle( GetCurrentProcess(), // Handle of process that thread // pseudohandle is relative to GetCurrentThread(), // Parent thread's pseudohandle GetCurrentProcess(), // Handle of process that the new, real, // thread handle is relative to &hThreadParent, // Will receive the new, real, handle // identifying the parent thread 0, // Ignored due to DUPLICATE_SAME_ACCESS FALSE, // New thread handle is not inheritable DUPLICATE_SAME_ACCESS); // New thread handle has same