Windows提供了多組支持多線程的應(yīng)用程序接口(API)函數(shù)。許多讀者已經(jīng)對Windows提供的多線程函數(shù)有一定程度的了解,但是對于那些不熟悉這些的讀者,本章提供了這些函數(shù)的概述。記住,Windows提供了許多其他的基于多線程的函數(shù),這些函數(shù)需要您自己去探索。
為了使用Windows的多線程函數(shù),必須在程序中包含<Windows.h>。
3.4.1 線程的創(chuàng)建和終止
Windows API提供了CreateThread()函數(shù)來創(chuàng)建一個線程。其原型如下所示:
HANDLE CreateThread(LPSECURITY_ATTRIBUTES secAttr,
SIZE_T stackSize,
LPTHREAD_START_ROUTINE threadFunc,
LPVOID param,
DWORD flags,
LPDWORD threadID);
在此,secAttr是一個用來描述線程的安全屬性的指針。如果secAttr是NULL,就會使用默認(rèn)的安全描述符。
每個線程都具有自己的堆棧。可以使用stackSize參數(shù)來按字節(jié)指定新線程堆棧的大小。如果這個整數(shù)值為0,那么這個線程堆棧的大小與創(chuàng)建它的線程相同。如果需要的話,這個堆棧可以擴展。(通常使用0來指定線程堆棧的大小)。
每個線程都在創(chuàng)建它的進程中通過調(diào)用線程函數(shù)來開始執(zhí)行。線程的執(zhí)行一直持續(xù)到線程函數(shù)返回。這個函數(shù)的地址(也就是線程的入口點)在threadFunc中指定。每個線程函數(shù)都必須具有這樣的原型:
DWORD WINAPI threadfunc(LPVOID param);
需要傳遞給新線程的任何參數(shù)都在CreateThread()的param中指定。線程函數(shù)在它的參數(shù)中接收這個32位的值。這個參數(shù)可以用作任何目的。函數(shù)返回它的退出狀態(tài)。
參數(shù)flags確定了線程的執(zhí)行狀態(tài)。如果它是0,線程會立即執(zhí)行。如果是CREATE_SUSPEND,線程則以掛起狀態(tài)創(chuàng)建并等待執(zhí)行。(可以通過調(diào)用ResumeThread()來開始執(zhí)行,稍后討論)。
與線程相關(guān)的標(biāo)識符以threadID所指向的長整型返回。
如果成功,函數(shù)則向線程返回一個句柄。如果失敗,則返回NULL。可以通過調(diào)用CloseHandle()來顯式銷毀這個線程。否則,會在父進程結(jié)束時自動銷毀它。
如前所述,當(dāng)線程的入口函數(shù)返回時終止執(zhí)行線程。進程也可以使用TerminateThread()或者ExitThread()來手動終止線程,這兩個函數(shù)的原型如下:
BOOL TerminateThread(HANDLE thread, DWORD status);
VOID ExitThread(DWORD status);
對于TerminateThread(),thread是將要終止的線程的句柄。ExitThread()只能用來終止調(diào)用了ExitThread()的線程。對于兩個函數(shù)而言,status是終止?fàn)顟B(tài)。TerminateThread()如果成功,則會返回非0值,否則返回0。
調(diào)用ExitThread()在功能上等價于允許線程函數(shù)正常返回。這意味著堆棧會正確地重新設(shè)置。當(dāng)使用TerminateThread()結(jié)束線程時,線程會立刻終止,而會執(zhí)行任何特定的清理任務(wù)。另外,TerminateThread()可能會停止正在執(zhí)行重要操作的線程。為此,當(dāng)入口函數(shù)返回時,通常最好(也是最容易的)讓線程正常終止。
3.4.2 Visual C++對CreateThread()和ExitThread()的替換
盡管CreateThread()和ExitThread()是用來創(chuàng)建并終止線程的Windows API函數(shù),我們在本章并不會使用它們。原因是在Visual C++中(其他的Windows兼容的編譯器也可能有這個問題)使用這兩個函數(shù)時,會導(dǎo)致內(nèi)存泄漏,丟失少量的內(nèi)存。對于Visual C++,如果多線程程序利用了C/C++標(biāo)準(zhǔn)庫函數(shù)并使用了CreateThread()和ExitThread(),就會丟失少量的內(nèi)存。(如果您的程序沒有使用C/C++的標(biāo)準(zhǔn)庫,就不會發(fā)生這樣的內(nèi)存丟失)。為了避免這種情況,必須使用Visual C++運行庫中定義的函數(shù)來開始和終止線程,而不是使用由Win32 API指定的函數(shù)。這些函數(shù)類似于CreateThread()和ExitThread(),但是不會產(chǎn)生內(nèi)存泄漏。
提示:
如果使用非Visual C++的編譯器,如果需要的話,檢查它的文檔來確定是否需要忽略CreateThread()和ExitThread(),以及如何做到這一點。
Visual C++用_beginthreadex()和_endthreadex()來取代CreateThread()和ExitThread()。這兩個函數(shù)都需要頭文件<process.h>。下面是_beginthreadex()函數(shù)的原型:
uintptr_t _beginthreadex(void *secAttr, unsigned stackSize,
unsigned (__stdcall *threadFunc)(void *),
void *param, unsigned flags,
unsigned *threadID);
正如您看到的那樣,_beginthreadex()的參數(shù)類似于CreateThread()的參數(shù)。另外,這些參數(shù)與CreateThread()指定的參數(shù)有相同的含義。secAttr是一個用來描述線程安全性屬性的指針。然而,如果secAttr為NULL,則會使用默認(rèn)的安全描述符。新線程堆棧的大小由stackSize參數(shù)按字節(jié)數(shù)傳遞。如果這個值為0,那么這個線程堆棧的大小與進程中創(chuàng)建它的主線程的大小相同。
線程函數(shù)的地址(也就是線程的入口點)在threadFunc中指定。對于_beginthreadex(),線程函數(shù)的原型如下:
unsigned_stdcall threadfunc(void *param)
這個原型在功能上等價于CreateThread()的線程函數(shù)的原型,但是它使用了不同的類型名稱。想要傳遞給新線程的任何參數(shù)都在param參數(shù)中指定。
flags參數(shù)確定線程的執(zhí)行狀態(tài)。如果flags參數(shù)為0,線程會立即開始執(zhí)行。如果flags參數(shù)為CREATE_SUSPEND,則以掛起狀態(tài)創(chuàng)建線程。(可以調(diào)用ResumeThread()來開始它)。與線程相關(guān)的標(biāo)識符以threadID指向的double word返回。
如果成功,則這個函數(shù)返回一個線程的句柄;如果失敗,則返回0。類型uintptr_t指定了可以擁有指針或者句柄的Visual C++類型。
_endthreadex()的原型如下:
void _endthreadex(unsigned status);
它的功能就像ExitThread()那樣,停止線程并返回status中指定的退出代碼(exit code)。
由于Windows下使用最廣泛的編譯器是Visual C++,因此本章示例將使用_beginthreadex()和_endthreadex()而不是使用它們的等價的API函數(shù)。如果您使用了非Visual C++的編譯器,那么只需要用CreateThread()和EndThread()來替代這兩個函數(shù)。
當(dāng)使用_beginthreadex()和_endthreadex()時,必須記住鏈接多線程庫。這隨編譯器的不同而不同。在此有一些示例。當(dāng)使用Visual C++的命令行編譯器時,包括-MT選項。為了在Visual C++ 6 IDE中使用多線程庫,首先要激活“Project | Settings”屬性頁。然后選擇“C/C++”選項卡。接著在“Category”下拉列表框中選擇“Code Generation”,然后在“Use Runtime Library ”下拉列表框中選擇“Multithreaded”。對于Visual C++ 7 .NET IDE,選擇“Project |Properties”。然后選擇“C/C++”條目,并加亮顯示“Code Generation”。最后,將“Multithreaded”選擇為運行庫。
3.4.3 線程的掛起和恢復(fù)
線程的執(zhí)行可以通過調(diào)用SuspendThread()來掛起。可以通過調(diào)用ResumeThread()來恢復(fù)它。這兩個函數(shù)的原型如下:
DWORD SuspendThread(HANDLE hThread);
DWORD ResumeThread(HANDLE hThread);
兩個函數(shù)都通過hThread來傳遞線程的句柄。
每個執(zhí)行的線程都有與其相關(guān)的掛起計數(shù)。如果這個計數(shù)為0,那么不會掛起線程。如果為非0值,則線程就會處于掛起狀態(tài)。每次調(diào)用SuspendThread()都會增加這個計數(shù)。每次調(diào)用ResumeThread()都會減小這個掛起計數(shù)。掛起的線程只有在它的掛起計數(shù)達(dá)到0時才會恢復(fù)。因此,為了恢復(fù)一個掛起的線程,對ResumeThread()的調(diào)用次數(shù)必須與對SuspendThread()的調(diào)用次數(shù)相等。
這兩個函數(shù)都返回線程先前的掛起計數(shù),如果發(fā)生錯誤,返回值為–1。
3.4.4 改變線程的優(yōu)先級
在Windows中,每個線程都與一個優(yōu)先級設(shè)置相關(guān)。線程的優(yōu)先級決定了線程接收的CPU時間的多少。低優(yōu)先級的線程接收比較少的時間,高優(yōu)先級的線程接收比較多的時間。當(dāng)然,線程接收的CPU時間的多少對于它的執(zhí)行性能以及它與系統(tǒng)中當(dāng)前執(zhí)行的其他線程之間的交互有著深遠(yuǎn)的影響。
在Windows中,線程優(yōu)先級的設(shè)置是兩個值的組合:進程總體的優(yōu)先級類別以及相對于這個優(yōu)先級類別的各個線程的優(yōu)先級設(shè)置。也就是說,線程實際的優(yōu)先級由進程的優(yōu)先級類別和各個線程的優(yōu)先級的組合來確定。后面會逐一講述。
1. 優(yōu)先級類別
在默認(rèn)情況下,進程具有普通的優(yōu)先級類別,大多數(shù)程序在其執(zhí)行的聲明周期內(nèi)保持這個普通的優(yōu)先級類別。盡管在本章的示例中沒有改變優(yōu)先級類別,但是為了完整起見,在此給出了線程優(yōu)先級類別的簡單概況。
Windows定義了6個優(yōu)先級類別,相應(yīng)的值以從高到低的順序顯示如下:
REALTIME_PRIORITY_CLASS
HIGH_PRIORITY_CLASS
ABOVE_NORMAL_PRIORITY_CLASS
NORMAL_PRIORITY_CLASS
BELOW_NORMAL_PRIORITY_CLASS
IDLE_PRIORITY_CLASS
在默認(rèn)情況下,程序的優(yōu)先級類別為NORMAL_PRIORITY_CLASS。通常,您不需要改變程序的優(yōu)先級類別。事實上,改變進程的優(yōu)先級類別對于整個計算機系統(tǒng)的性能會有負(fù)面的影響。例如,如果您將一個程序的優(yōu)先級類別增加到REALTIME_PRIORITY_CLASS,它就會支配CPU。對于某些特殊的應(yīng)用程序,可能需要增加應(yīng)用程序的優(yōu)先級類別,但通常并不需要。如前所述,本章的應(yīng)用程序沒有改變優(yōu)先級類別。
當(dāng)確實需要改變程序的優(yōu)先級類別時,可以調(diào)用SetPriorityClass()。可以調(diào)用GetPriorityClass()來獲取當(dāng)前的優(yōu)先級類別。這兩個函數(shù)的原型如下:
DWORD GetPriorityClass(HANDLE hApp);
BOOL SetPriorityClass(HANDLE hApp, DWORD priority);
在此,hApp是進程的句柄。GetPriorityClass()返回應(yīng)用程序的優(yōu)先級類別,如果失敗的話,返回0。對于SetPriorityClass(),priority指定了進程的新優(yōu)先級類別。
2. 線程優(yōu)先級
對于給定的優(yōu)先級類別,各個線程的優(yōu)先級確定了它在進程內(nèi)接收的CPU時間的多少。當(dāng)線程第一次創(chuàng)建時,它具有普通的優(yōu)先級,但是您可以改變線程的優(yōu)先級—— 即使在它執(zhí) 行時。
可以通過調(diào)用GetThreadPriority()來獲取線程的優(yōu)先級設(shè)置。可以使用SetThreadPriority()來增加或者減小線程的優(yōu)先級。這兩個函數(shù)的原型如下:
BOOL SetThreadPriority(HANDLE hThread, int priority);
int GetThreadPriority(HANDLE hThread);
對于這兩個函數(shù)而言,hThread是線程的句柄。對于SetThreadPriority(),priority是新的優(yōu)先級設(shè)置。如果發(fā)生錯誤,則返回值為0;否則,返回非0值。GetThreadPriority()會返回當(dāng)前的優(yōu)先級設(shè)置。優(yōu)先級設(shè)置按照從高到低的順序如表3-1所示。
表3-1 優(yōu)先級設(shè)置
線程優(yōu)先級
|
值
|
THREAD_PRIORITY_TIME_CRITICAL
|
15
|
THREAD_PRIORITY_HIGHEST
|
2
|
THREAD_PRIORITY_ABOVE_NORMAL
|
1
|
THREAD_PRIORITY_NORMAL
|
0
|
THREAD_PRIORITY_BELOW_NORMAL
|
-1
|
THREAD_PRIORITY_LOWEST
|
-2
|
THREAD_PRIORITY_IDLE
|
-15
|
這些值相對于進程的優(yōu)先級類別或增或減。通過組合進程的優(yōu)先級類別和線程的優(yōu)先級,Windows向應(yīng)用程序提供了31個不同的優(yōu)先級設(shè)置的支持。
如果有錯誤發(fā)生,則GetThreadPriority()返回THREAD_PRIORITY_ERROR_RETURN。
在大多數(shù)情況下,如果線程具有普通的優(yōu)先級類別,那么可以隨意地改變它的優(yōu)先級設(shè)置,而不必?fù)?dān)心會給整個系統(tǒng)的性能帶來災(zāi)難性的影響。您將會看到,在下面部分開發(fā)的線程控制面板中,可以改變進程內(nèi)線程的優(yōu)先級設(shè)置(但是不能改變優(yōu)先級類別)。
3.4.5 獲取主線程的句柄
主線程的執(zhí)行是可以控制的。為此,需要獲取它的句柄。做到這一點最簡單的方法是調(diào)用GetCurrentThread(),其原型如下:
HANDLE GetCurrentThread(void);
這個函數(shù)返回當(dāng)前線程的偽句柄(pseudohandle)。之所以稱之為偽句柄,是因為它是一個預(yù)定義的值,總是引用當(dāng)前的線程,而不是引用指定的調(diào)用線程。然而,它能夠用在任何可以使用普通線程處理的地方。
3.4.6 同步
在使用多線程或多進程時,有時需要調(diào)整兩個或者多個線程(或者進程)之間的活動。這個過程稱為同步。當(dāng)兩個或者多個線程需要訪問共享資源,而這個共享資源在同一時刻只能由一個線程使用時,就需要使用同步。例如,當(dāng)一個線程在寫文件時,在此時必須阻止另一個線程也這么做。同步的另一個原因是有時線程需要等待由另一個線程引發(fā)的事件。在此情況下,必須采取某種措施將第一個線程保持掛起狀態(tài),直到這個事件發(fā)生。隨后等待的線程必須恢復(fù) 執(zhí)行。
通常某個任務(wù)會處于兩種狀態(tài)。首先,它可能正在執(zhí)行(或者在獲得它的時間段時就開始執(zhí)行)。另外,任務(wù)可能被阻塞,等待某個資源或者事件。在此情況下其執(zhí)行被掛起,直到所需的資源可以使用或者所等待的事件發(fā)生。
如果您對于同步問題或者它的常用解決方案(信號量)不熟悉,下面的部分將對此進行討論。
1. 理解同步問題
Windows必須提供某種特殊的服務(wù)來允許對共享資源的訪問同步,因為如果沒有操作系統(tǒng)的協(xié)助,進程或者線程就沒有辦法得知它是否在單獨訪問某個資源。為了理解這個問題,假定您在為一個沒有提供任何同步支持的多任務(wù)操作系統(tǒng)編寫程序,并且假定您具有兩個并發(fā)執(zhí)行的線程A和B,它們都不時地訪問某個資源R(如磁盤文件),這個資源在某個時刻只能被一個線程訪問。為了在一個線程使用這個資源時阻止另一個線程訪問它,您嘗試了下面的解決方案。首先,創(chuàng)建了一個初始化值為0并且兩個線程都可以訪問的變量,名為flag。然后,在使用訪問R的每段代碼之前,等待flag被清0,然后設(shè)置flag,訪問R,最后將flag清0。也就是說,在每個線程訪問R之前,執(zhí)行如下的代碼:
while(flag) ; // wait for flag to be cleared
flag = 1; // set flag
// ... access resource R ...
flag = 0; // clear the flag
這段代碼隱含的概念是,如果設(shè)置了flag,則兩個線程都不能夠訪問R。從概念上講,這種方法是正確的解決方案。然而,實際上它遠(yuǎn)遠(yuǎn)沒有達(dá)到要求,原因很簡單:它并非總是有效!讓我們看一下原因。
使用剛才給定的代碼,有可能兩個進程同時訪問R。while循環(huán)在本質(zhì)上執(zhí)行重復(fù)的加載和比較flag上的指令。換句話說,它一直在測試flag的值。當(dāng)flag被清0的時候,代碼的下一行將設(shè)置flag的值。問題在于,這兩個操作有可能在兩個不同的時間段執(zhí)行。在兩個時間段之間,flag的值有可能被另一個線程訪問,從而R被兩個線程同時訪問。為了理解這一點,假定線程A進入while循環(huán),發(fā)現(xiàn)flag為0,這是訪問R的綠燈。然而,在將flag設(shè)置為1之前,其時間段用盡,線程B恢復(fù)執(zhí)行。如果B執(zhí)行了它的while,它也發(fā)現(xiàn)flag沒有被設(shè)置,并且認(rèn)為它可以安全地訪問R。然而,當(dāng)A重新開始時,它也會訪問R。問題的關(guān)鍵在于對flag的測試和設(shè)置沒有包含在一個連續(xù)的操作中,而是可以被分為兩個部分,正如剛才說明的那樣。無論您如何努力,都沒有辦法只使用應(yīng)用層的代碼以絕對保證在同一時刻只有一個線程訪問R。
對同步問題的解決方案簡單而優(yōu)雅。操作系統(tǒng)(在Windows中)提供了一個例程,在一個連續(xù)的操作中完成對flag的測試和設(shè)置(如果可能的話)。用操作系統(tǒng)工程師的話來說,這就是所謂的測試和置位(test and set)操作。由于歷史的原因,用來控制對共享資源的訪問并提供線程(以及進程)間同步的標(biāo)記被稱為信號量。信號量是Windows同步系統(tǒng)的核心。
2. Windows的同步對象
Windows支持幾種類型的同步對象。第一種類型是經(jīng)典的信號量。當(dāng)使用信號量時,可以完全同步資源,在此情況下只有一個進程或者線程在同一時刻可以訪問這個資源,或者信號量允許不超過一定數(shù)量的進程或者線程在同一時刻訪問資源。信號量使用計數(shù)器來實現(xiàn),當(dāng)某個任務(wù)使用信號量時,計數(shù)器減小;當(dāng)這個任務(wù)釋放信號量時,計數(shù)器增加。
第二個同步對象是互斥體信號量,或者簡稱為互斥體。互斥體將一個資源同步,保證在任何時候都只有一個線程或者進程來訪問它。在本質(zhì)上,互斥體是標(biāo)準(zhǔn)信號量的一個特殊版本。
第三個同步對象是事件對象。它可以用來阻塞對某個資源的訪問,直到某個其他的進程或者線程發(fā)送信號,通知可以使用資源(也就是一個事件對象發(fā)送某個指定的事件發(fā)生的信號)。
第四個同步對象是可等待計時器。可等待計時器阻塞線程的執(zhí)行,直到指定的時間。也可以創(chuàng)建計時器序列,這是一個計時器的列表。
可以使用臨界區(qū)對象將一個代碼段放入臨界區(qū),從而阻止在同一時刻一個以上的線程使用這段代碼。當(dāng)一個線程進入臨界區(qū)時,其他線程只有在第一個線程離開整個臨界區(qū)時才可以使用它。
本章使用的惟一的同步對象是互斥體,下面的部分將對其進行描述。然而,C++程序員可以使用所有的Windows定義的同步對象。如前所述,這是使得C++依賴于操作系統(tǒng)處理多線程的主要優(yōu)點之一:所有的多線程特性都在您的控制之中。
3. 使用互斥體同步線程
如前所述,互斥體是一種特殊的信號量,在給定的時間內(nèi),只允許一個線程訪問某個資源。在使用互斥體之前,必須使用CreatMutex()創(chuàng)建一個互斥體,函數(shù)原型如下:
HANDLE CreateMutex(LPSECURITY_ATTRIBUTES secAttr,
BOOL acquire,
LPCSTR name);
在此,secAttr是用來描述安全屬性的指針。如果secAttr為NULL,則使用默認(rèn)的安全描 述符。
如果創(chuàng)建的線程需要互斥體的控制,則acquire為true,否則為false。
name參數(shù)指向一個字符串,這個字符串是互斥體對象的名稱。互斥體是一個全局對象,它可能被其他進程使用。為此,當(dāng)兩個進程都打開了使用相同名稱的互斥體時,二者引用了相同的互斥體。使用這種方法可以將兩個進程同步。這個名稱也可以為NULL,在此情況下這個信號量被限制在一個進程之內(nèi)。
如果成功,則CreatMutex()函數(shù)返回信號量的句柄,否則,返回NULL。當(dāng)主進程結(jié)束時,互斥體的句柄則自動關(guān)閉。當(dāng)不再需要時,可以調(diào)用CloseHandle()來顯式地關(guān)閉互斥體的句柄。
當(dāng)創(chuàng)建信號量時,可以調(diào)用兩個相關(guān)的函數(shù)來使用它:WaitForSingleObject()和ReleaseMutex()。這兩個函數(shù)的原型如下:
DWORD WaitForSingleObject(HANDLE hObject, DWORD howLong);
BOOL ReleaseMutex(HANDLE hMutex);
WaitForSingleObject()等待一個同步對象,直到這個對象可以使用或者超時之后才會返回。在使用互斥體時,hObject是互斥體的句柄。howLong參數(shù)以毫秒為單位指定調(diào)用例程的等待時間。當(dāng)這個時間用盡時,會返回超時錯誤。為了無限期地等待,可以使用值INFINITE。當(dāng)成功時(也就是訪問被準(zhǔn)許),這個函數(shù)返回WAIT_OBJECT_0。當(dāng)發(fā)生超時時,返回WAIT_TIMEOUT。
ReleaseMutex()釋放互斥體,并允許其他線程獲取它。在此,hMutex是互斥體的句柄。如果成功,則函數(shù)返回非0值;如果失敗,則返回0。
為了使用互斥體控制對共享資源的訪問,封裝了訪問在調(diào)用WaitForSingleObject()和ReleaseMutex()之間的資源的代碼,如下面的代碼所示(當(dāng)然,超時期限隨應(yīng)用程序的不同而 不同)。
if(WaitForSingleObject(hMutex, 10000)==WAIT_TIMEOUT) {
// handle time-out error
}
// access the resource
ReleaseMutex(hMutex);
通常,您會選擇足夠長的超時期限來適應(yīng)程序的操作。如果在開發(fā)多線程應(yīng)用程序時重復(fù)出現(xiàn)超時錯誤,那么通常意味著您創(chuàng)建了死鎖條件。當(dāng)一個線程等待另一個線程永遠(yuǎn)都不會釋放的互斥體時,就會發(fā)生死鎖。