多線程程序設計(二)

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函數

發佈了24 篇原創文章 · 獲贊 18 · 訪問量 31萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章