4.退出代碼Exit Code
成員Exit Code指定了線程的退出代碼,也可以說是線程函數的返回值。在線程運行期間,線程函數還沒有返回,Exit Code的值是STILL_ACTIVE。線程運行結束後,系統自動將ExitCode設爲線程函數的返回值。可以用GetExitCodeThread函數得到線程的退出代碼。
……
DWORD dwExitCode;
if(::GetExitCodeThread(hThread, &dwExitCode))
{ if(dwExitCode == STILL_ACTIVE)
{ } // 目標線程還在運行
else
{ } // 目標線程已經中止,退出代碼爲dwExitCode
}
……
5.是否受信Signaled
成員Signaled指示了線程對象是否爲“受信”狀態。線程在運行期間,Signaled的值永遠是FALSE,即“未受信”,只有當線程結束以後,系統才把Signaled的值置爲TRUE。此時,針對此對象的等待函數就會返回,如上一小節中的WaitForSingleObject函數。
3.1.3 線程的終止
當線程正常終止時,會發生下列事件:
l 在線程函數中創建的所有C++對象將通過它們各自的析構函數被正確地銷燬。
l 該線程使用的堆棧將被釋放。
l 系統將線程內核對象中Exit Code(退出代碼)的值由STILL_ACTIVE設置爲線程函數的返回值。
l 系統將遞減線程內核對象中Usage Code(使用計數)的值。
線程結束後的退出代碼可以被其他線程用GetExitCodeThread函數檢測到,所以可以當做自定義的返回值來表示線程的執行結果。終止線程的執行有4種方法。
(1)線程函數自然退出。當函數執行到return語句返回時,Windows將終止線程的執行。建議使用這種方法終止線程的執行。
(2)使用ExitThread函數來終止線程,原型如下:
void ExitThread( DWORD dwExitCode); // 線程的退出代碼
ExitThread函數會中止當前線程的運行,促使系統釋放掉所有此線程使用的資源。但是,C/C++資源卻不能得到正確地清除。例如,在下面一段代碼中,theObject對象的析構函數就不會被調用。
class CMyClass
{
public:
CMyClass() { printf(" Constructor\n"); }
~CMyClass() { printf(" Destructor\n"); }
};
void main()
{ CMyClass theObject;
::ExitThread(0); // ExitThread函數使線程立刻中止,theObject對象的析構函數得不到機會被調用
// 在函數的結尾,編譯器會自動添加一些必要的代碼,來調用theObject的析構函數
}
運行上面的代碼,將會看到程序的輸出。
Constructor
一個對象被創建,但是永遠也看不到Destructor這個單詞出現。theObject這個C++對象沒有被正確地銷燬,原因是ExitThread函數強制該線程立刻終止,C/C++運行期沒有機會執行清除代碼。
所以結束線程最好的方法是讓線程函數自然返回。如果在上面的代碼中刪除了對ExitThread的調用,再次運行程序產生的輸出結果如下:
Constructor
Destructor
(3)使用TerminateThread函數在一個線程中強制終止另一個線程的執行,原型如下:
BOOL TerminateThread(
HANDLE hThread, // 目標線程句柄
DWORD dwExitCode // 目標線程的退出代碼
);
這是一個被強烈建議避免使用的函數,因爲一旦執行這個函數,程序無法預測目標線程會在何處被終止,其結果就是目標線程可能根本沒有機會來做清除工作,比如,線程中打開的文件和申請的內存都不會被釋放。另外,使用TerminateThread函數終止線程的時候,系統不會釋放線程使用的堆棧。所以,建議讀者在編程的時候儘量讓線程自己退出。如果主線程要求某個線程結束,可以通過各種方法通知線程,線程收到通知後自行退出。只有在迫不得已的情況下,才使用TerminateThread函數終止線程。
(4)使用ExitProcess函數結束進程,這時系統會自動結束進程中所有線程的運行。用這種方法相當於對每個線程使用TerminateThread函數,所以也應當避免這種情況。
總之,始終應該讓線程正常退出,即由它的線程函數返回。通知線程退出的方法很多,如使用事件對象、設置全局變量等,這是下一節的話題。
3.1.4 線程的優先級
每個線程都要被賦予一個優先級號,取值爲0(最低)到31(最高)。當系統確定哪個線程需要分配CPU時,它先檢查優先級爲31的線程,然後以循環的方式對他們進行調度。如果有一個優先級爲31的線程可調度,它就會被分配到一個CPU上運行。在該線程的時間片結束時,系統查看是否還有另一個優先級爲31的線程,如果有,就安排這個線程到CPU上運行。
Windows調度線程的原則就是這樣的,只要優先級爲31的線程是可調度的,就絕對不會將優先級爲0~30的線程分配給CPU。大家可能以爲,在這樣的系統中,低優先級的線程永遠得不到機會運行。事實上,在任何一段時間內,系統中的線程大多是不可調度的,即處於暫停狀態。比如3.1.1小節的例子中,調用WaitForSingleObject函數就會導致主線程處於不可調度狀態,還有在第4章要討論的GetMessage函數,也會使線程暫停運行。
Windows支持6個優先級類:idle、below normal、normal、above normal、high和real-time。從字面上也可以看出,normal是被絕大多數應用程序採用的優先級類。其實,進程也是有優先級的,只是在實際的開發過程中很少使用而已。進程屬於一個優先級類,還可以爲進程中的線程賦予一個相對線程優先級。但是,一般情況下並不改變進程的優先級(默認是nomal),所以可以認爲,線程的相對優先級就是它的真實優先級,與其所在的進程的優先級類無關。
線程剛被創建時,他的相對優先級總是被設置爲normal。若要改變線程的優先級,必須使用下面這個函數:
BOOL SetThreadPriority(HANDLE hThread,int nPriority );
hThread參數是目標線程的句柄,nPriority參數定義了線程的優先級,取值如下所示:
l THREAD_PRIORITY_TIME_CRITICAL Time-critical(實時)
l THREAD_PRIORITY_HIGHEST Highest(最高)
l THREAD_PRIORITY_ABOVE_NORMAL Above normal(高於正常,Windows 98不支持)
l THREAD_PRIORITY_NORMAL Normal(正常)
l THREAD_PRIORITY_BELOW_NORMAL Below normal(低於正常,Windows 98不支持)
l THREAD_PRIORITY_LOWEST Lowest(最低)
l THREAD_PRIORITY_IDLE Idle(空閒)
下面的小例子說明了優先級的不同給線程帶來的影響。它同時創建了兩個線程,一個線程的優先級是“空閒”,運行的時候不斷打印出“Idle Thread is running”;另一個線程的優先級爲“正常”,運行的時候不斷打印出“Normal Thread is running”字符串。源程序代碼如下:
DWORD WINAPI ThreadIdle(LPVOID lpParam) // 03PriorityDemo工程下
{ int i = 0;
while(i++<10)
printf("Idle Thread is running \n");
return 0;
}
DWORD WINAPI ThreadNormal(LPVOID lpParam)
{ int i = 0;
while(i++<10)
printf(" Normal Thread is running \n");
return 0;
}
int main(int argc, char* argv[])
{ DWORD dwThreadID;
HANDLE h[2];
// 創建一個優先級爲Idle的線程
h[0] = ::CreateThread(NULL, 0, ThreadIdle, NULL,
CREATE_SUSPENDED, &dwThreadID);
::SetThreadPriority(h[0], THREAD_PRIORITY_IDLE);
::ResumeThread(h[0]);
// 創建一個優先級爲Normal的線程
h[1] = ::CreateThread(NULL, 0, ThreadNormal, NULL,
0, &dwThreadID);
// 等待兩個線程內核對象都變成受信狀態
::WaitForMultipleObjects(
2, // DWORD nCount 要等待的內核對象的數量
h, // CONST HANDLE *lpHandles 句柄數組
TRUE, // BOOL bWaitAll 指定是否等待所有內核對象變成受信狀態
INFINITE); // DWORD dwMilliseconds 要等待的時間
::CloseHandle(h[0]);
::CloseHandle(h[1]);
return 0;
}
程序運行結果如圖3.2所示。可以看到,只要有優先級高的線程處於可調度狀態,Windows是不允許優先級相對低的線程佔用CPU的。
3.2 兩個優先級不同的線程
創建第一個線程時,將CREATE_SUSPENDED標記傳給了CreateThread函數,這可以使新線程處於暫停狀態。在將它的優先級設爲THREAD_PRIORITY_IDLE後,再調用ResumeThread函數恢復線程運行。這種改變線程優先級的方法在實際編程過程中經常用到。
WaitForMultipleObjects函數用於等待多個內核對象,前兩個參數分別爲要等待的內核對象的個數和句柄數組指針。如果將第三個參數bWaitAll的值設爲TRUE,等待的內核對象全部變成受信狀態以後此函數才返回。否則,bWaitAll爲0的話,只要等待的內核對象中有一個變成了受信狀態,WaitForMultipleObjects就返回,返回值指明瞭是哪一個內核對象變成了受信狀態。下面的代碼說明了函數返回值的作用:
HANDLE h[2];
h[0] = hThread1;
h[1] = hThread2;
DWORD dw = ::WaitForMultipleObjects(2, h, FALSE, 5000);
switch(dw)
{ case WAIT_FAILED:
// 調用WaitForMultipleObjects函數失敗(句柄無效?)
break;
case WAIT_TIMEOUT:
// 在5秒內沒有一個內核對象受信
break;
case WAIT_OBJECT_0 + 0:
// 句柄h[0]對應的內核對象受信
break;
case WAIT_OBJECT_0 + 1:
// 句柄h[1]對應的內核對象受信
break;
}
參數bWaitAll爲FALSE的時候,WaitForMultipleObjects函數從索引0開始掃描整個句柄數組,第一個受信的內核對象將終止函數的等待,使函數返回。
有的時候使用高優先級的線程是非常必要的。比如,Windows Explorer進程中的線程就是在高優先級下運行的。大部分時間裏,Explorer的線程都處於暫停狀態,等待接受用戶的輸入。當Explorer的線程被掛起的時候,系統不給它們安排CPU時間片,使其他低優先級的線程佔用CPU。但是,一旦用戶按下一個鍵或組合鍵,例如Ctrl+Esc,系統就喚醒Explorer的線程(用戶按Ctrl+Esc時,開始菜單將出現)。如果該時刻有其他優先級低的線程正在運行的話,系統會立刻掛起這些線程,允許Explorer的線程運行。這就是搶佔式優先操作系統。
3.1.5 C/C++運行期庫
在實際的開發過程中,一般不直接使用Windows系統提供的CreateThread函數創建線程,而是使用C/C++運行期函數_beginthreadex。本小節主要來分析一下_beginthreadex函數的內部實現。
事實上,C/C++運行期庫提供另一個版本的CreateThread是爲了多線程同步的需要。在標準運行庫裏面有許多全局變量,如errno、strerror等,它們可以用來表示線程當前的狀態。但是在多線程程序設計中,每個線程必須有惟一的狀態,否則這些變量記錄的信息就不會準確了。比如,全局變量errno用於表示調用運行期函數失敗後的錯誤代碼。如果所有線程共享一個errno的話,在一個線程產生的錯誤代碼就會影響到另一個線程。爲了解決這個問題,每個線程都需要有自己的errno變量。
要想使運行期爲每個線程都設置狀態變量,必須在創建線程的時候調用運行期提供的_beginthreadex,讓運行期設置了相關變量後再去調用Windows系統提供的CreateThread函數。_beginthreadex的參數與CreateThread函數是對應的,只是參數名和類型不完全相同,使用的時候需要強制轉化。
unsigned long _beginthreadex(
void *security,
unsigned stack_size,
unsigned ( __stdcall *start_address )( void * ),
void *arglist,
unsigned initflag,
unsigned *thrdaddr
);
VC++默認的C/C++運行期庫並不支持_beginthreadex函數。這是因爲標準C運行期庫是在1970年左右問世的,那個時候還沒有多線程這個概念,也就沒有考慮到將C運行期庫用於多線程應用程序所出現的問題。要想使用_beginthreadex函數,必須對VC進行設置,更換它默認使用的運行期庫。
選擇菜單命令“Project/Settings…”,打開標題爲“Project Settings”的對話框,如圖3.3所示。選中C/C++選項卡,在Category對應的組合框中選擇Code Generation類別。從Use run-time library組合框中選定6個選項中的一個。默認的選擇是第一個,即Single-Threaded,此選項對應着單線程應用程序的靜態鏈接庫。爲了使用多線程,選中Multithreaded DLL就可以了。後兩節的例子就使用_beginthreadex函數來創建線程。
圖3.3 選擇支持多線程的運行期庫
相應地,C/C++運行期庫也提供了另一個版本的結束當前線程運行的函數,用於取代ExitThread函數。
void _endthreadex(unsigned retval ); // 指定退出代碼
這個函數會釋放_beginthreadex爲保持線程同步而申請的內存空間,然後再調用ExitThread函數來終止線程。同樣,筆者還是建議讓線程自然退出,而不要使用_endthreadex函數