-
-
- Win32的線程
- 線程的創建
使用CreateThread函數創建線程,CreateThread的原型如下:
HANDLE CreateThread(
LPSECURITY_ATTRIBUTES lpThreadAttributes,
DWORD dwStackSize,
LPTHREAD_START_ROUTINE lpStartAddress,
LPVOID lpParameter,
DWORD dwCreationFlags, // creation flags
LPDWORD lpThreadId
);
其中:
lpThreadAttributes表示創建線程的安全屬性,NT下有用。
dwStackSize指定線程棧的尺寸,如果爲0則與進程主線程棧相同。
lpStartAddress指定線程開始運行的地址。
lpParameter表示傳遞給線程的32位的參數。
dwCreateFlages表示是否創建後掛起線程(取值CREATE_SUSPEND),掛起後調用ResumeThread繼續執行。
lpThreadId用來存放返回的線程ID。
線程的優先級別
進程的每個優先級類包含了五個線程的優先級水平。在進程的優先級類確定之後,可以改變線程的優先級水平。用SetPriorityClass設置進程優先級類,用SetThreadPriority設置線程優先級水平。
Normal級的線程可以被除了Idle級以外的任意線程搶佔。
-
-
- 線程的終止
以下情況終止一個線程:
調用了ExitThread函數;
線程函數返回:主線程返回導致ExitProcess被調用,其他線程返回導致ExitThread被調用;
調用ExitProcess導致進程的所有線程終止;
調用TerminateThread終止一個線程;
調用TerminateProcess終止一個進程時,導致其所有線程的終止。
當用TerminateProcess或者TerminateThread終止進程或線程時,DLL的入口函數DllMain不會被執行(如果有DLL的話)。
-
-
- 線程局部存儲
如果希望每個線程都可以有線程局部(Thread local)的靜態存儲數據,可以使用TLS線程局部存儲技術。TLS爲進程分配一個TLS索引,進程的每個線程通過這個索引存取自己的數據變量的拷貝。
TLS對DLL是非常有用的。當一個新的進程使用DLL時,在DLL入口函數DllMain中使用TlsAlloc分配TLS索引,TLS索引就作爲進程私有的全局變量被保存;以後,當該進程的新的線程使用DLL時(Attahced to DLL),DllMain給它分配動態內存並且使用TlsSetValue把線程私有的數據按索引保存。DLL函數可以使用TlsGetValue按索引讀取調用線程的私有數據。
TLS函數如下:
DWORD TlsAlloc()
在進程或DLL初始化時調用,並且把返回值(索引值)作爲全局變量保存。
BOOL TlsSetValue(
DWORD dwTlsIndex, //TLS index to set value for
LPVOID lpTlsValue //value to be stored
);
其中:
dwTlsIndex是TlsAlloc分配的索引。
lpTlsValue是線程在TLS槽中存放的數據指針,指針指向線程要保存的數據。
線程首先分配動態內存並保存數據到此內存中,然後調用TlsSetValue保存內存指針到TLS槽。
LPVOID TlsGetValue(
DWORD dwTlsIndex // TLS index to retrieve value for
);
其中:
dwTlsIndex是TlsAlloc分配的索引。
當要存取保存的數據時,使用索引得到數據指針。
BOOL TlsFree(
DWORD dwTlsIndex // TLS index to free
);
其中:
dwTlsIndex是TlsAlloc分配的索引。
當每一個線程都不再使用局部存儲數據時,線程釋放它分配的動態內存。在TLS索引不再需要時,使用TlsFree釋放索引。
-
- 線程同步
同步可以保證在一個時間內只有一個線程對某個資源(如操作系統資源等共享資源)有控制權。共享資源包括全局變量、公共數據成員或者句柄等。同步還可以使得有關聯交互作用的代碼按一定的順序執行。
Win32提供了一組對象用來實現多線程的同步。
這些對象有兩種狀態:獲得信號(Signaled)或者沒有或則信號(Not signaled)。線程通過Win32 API提供的同步等待函數(Wait functions)來使用同步對象。一個同步對象在同步等待函數調用時被指定,調用同步函數地線程被阻塞(blocked),直到同步對象獲得信號。被阻塞的線程不佔用CPU時間。
- 同步對象
同步對象有:Critical_section(關鍵段),Event(事件),Mutex(互斥對象),Semaphores(信號量)。
下面,解釋怎麼使用這些同步對象。
關鍵段對象:
首先,定義一個關鍵段對象cs:
CRITICAL_SECTION cs;
然後,初始化該對象。初始化時把對象設置爲NOT_SINGALED,表示允許線程使用資源:
InitializeCriticalSection(&cs);
如果一段程序代碼需要對某個資源進行同步保護,則這是一段關鍵段代碼。在進入該關鍵段代碼前調用EnterCriticalSection函數,這樣,其他線程都不能執行該段代碼,若它們試圖執行就會被阻塞。
完成關鍵段的執行之後,調用LeaveCriticalSection函數,其他的線程就可以繼續執行該段代碼。如果該函數不被調用,則其他線程將無限期的等待。
事件對象
首先,調用CreateEvent函數創建一個事件對象,該函數返回一個事件句柄。然後,可以設置(SetEvent)或者復位(ResetEvent)一個事件對象,也可以發一個事件脈衝(PlusEvent),即設置一個事件對象,然後復位它。復位有兩種形式:自動復位和人工復位。在創建事件對象時指定復位形式。。
自動復位:當對象獲得信號後,就釋放下一個可用線程(優先級別最高的線程;如果優先級別相同,則等待隊列中的第一個線程被釋放)。
人工復位:當對象獲得信號後,就釋放所有可利用線程。
最後,使用CloseHandle銷燬創建的事件對象。
互斥對象
首先,調用CreateMutex創建互斥對象;然後,調用等待函數,可以的話利用關鍵資源;最後,調用RealseMutex釋放互斥對象。
互斥對象可以在進程間使用,但關鍵段對象只能用於同一進程的線程之間。
信號量對象
在Win32中,信號量的數值變爲0時給以信號。在有多個資源需要管理時可以使用信號量對象。
首先,調用CreateSemaphore創建一個信號量;然後,調用等待函數,如果允許的話,則利用關鍵資源;最後,調用RealeaseSemaphore釋放信號量對象。
此外,還有其他句柄可以用來同步線程:
文件句柄(FILE HANDLES)
命名管道句柄(NAMED PIPE HANDELS)
控制檯輸入緩衝區句柄(CONSOLE INPUT BUFFER HANDLES)
通訊設備句柄(COMMUNICTION DEVICE HANDLES)
進程句柄(PROCESS HANDLES)
線程句柄(THREAD HANDLES)
例如,當一個進程或線程結束時,進程或線程句柄獲得信號,等待該進程或者線程結束的線程被釋放。
-
-
- 等待函數
Win32提供了一組等待函數用來讓一個線程阻塞自己的執行。等待函數分三類:
等待單個對象的(FOR SINGLE OBJECT):
這類函數包括:
SignalObjectAndWait
WaitForSingleObject
WaitForSingleObjectEx
函數參數包括同步對象的句柄和等待時間等。
在以下情況下等待函數返回:
同步對象獲得信號時返回;
等待時間達到了返回:如果等待時間不限制(Infinite),則只有同步對象獲得信號才返回;如果等待時間爲0,則在測試了同步對象的狀態之後馬上返回。
等待多個對象的(FOR MULTIPLE OBJECTS)
這類函數包括:
WaitForMultipleObjects
WaitForMultipleObjectsEx
MsgWaitForMultipleObjects
MsgWaitForMultipleObjectsEx
函數參數包括同步對象的句柄,等待時間,是等待一個還是多個同步對象等等。
在以下情況下等待函數返回:
一個或全部同步對象獲得信號時返回(在參數中指定是等待一個或多個同步對象);
等待時間達到了返回:如果等待時間不限制(Infinite),則只有同步對象獲得信號才返回;如果等待時間爲0,則在測試了同步對象的狀態之後馬上返回。
可以發出提示的函數(ALTERABLE)
這類函數包括:
MsgWaitForMultipleObjectsEx
SignalObjectAndWait
WaitForMultipleObjectsEx
WaitForSingleObjectEx
這些函數主要用於重疊(Overlapped)的I/O(異步I/O)。
-
- MFC的線程處理
在Win32 API的基礎之上,MFC提供了處理線程的類和函數。處理線程的類是CWinThread,函數是AfxBeginThread、AfxEndThread等。
表5-6解釋了CWinThread的成員變量和函數。
CWinThread是MFC線程類,它的成員變量m_hThread和m_hThreadID是對應的Win32線程句柄和線程ID。
MFC明確區分兩種線程:用戶界面線程(User interface thread)和工作者線程(Worker thread)。用戶界面線程一般用於處理用戶輸入並對用戶產生的事件和消息作出應答。工作者線程用於完成不要求用戶輸入的任務,如耗時計算。
Win32 API並不區分線程類型,它只需要知道線程的開始地址以便它開始執行線程。MFC爲用戶界面線程特別地提供消息泵來處理用戶界面的事件。CWinApp對象是用戶界面線程對象的一個例子,CWinApp從類CWinThread派生並處理用戶產生的事件和消息。
- 創建用戶界面線程
通過以下步驟創建一個用戶界面線程:
從CWinThread派生一個有動態創建能力的類。使用DECLARE_DYNCREATE和IMPLEMENT_DYNCREATE宏來支持動態創建。
覆蓋CWinThread的一些虛擬函數,可以覆蓋的函數見表5-4關於CWinThread的部分。其中,函數InitInstance是必須覆蓋的,ExitInstance通常是要覆蓋的。
使用AfxBeginThread創建MFC線程對象和Win32線程對象。如果創建線程時沒有指定CREATE_SUSPENDED,則開始執行線程。
如果創建線程是指定了CREATE_SUSPENDED,則在適當的地方調用函數ResumeThread開始執行線程。
-
-
- 創建工作者線程
程序員不必從CWinThread派生新的線程類,只需要提供一個控制函數,由線程啓動後執行該函數。
然後,使用AfxBeginThread創建MFC線程對象和Win32線程對象。如果創建線程時沒有指定CREATE_SUSPENDED(創建後掛起),則創建的新線程開始執行。
如果創建線程是指定了CREATE_SUSPENDED,則在適當的地方調用函數ResumeThread開始執行線程。
雖然程序員沒有從CWinThread派生類,但是MFC給工作者線程提供了缺省的CWinThread對象。
- AfxBeginThread
用戶界面線程和工作者線程都是由AfxBeginThread創建的。現在,考察該函數:MFC提供了兩個重載版的AfxBeginThread,一個用於用戶界面線程,另一個用於工作者線程,分別有如下的原型和過程:
用戶界面線程的AfxBeginThread
用戶界面線程的AfxBeginThread的原型如下:
CWinThread* AFXAPI AfxBeginThread(
CRuntimeClass* pThreadClass,
int nPriority,
UINT nStackSize,
DWORD dwCreateFlags,
LPSECURITY_ATTRIBUTES lpSecurityAttrs)
其中:
參數1是從CWinThread派生的RUNTIME_CLASS類;
參數2指定線程優先級,如果爲0,則與創建該線程的線程相同;
參數3指定線程的堆棧大小,如果爲0,則與創建該線程的線程相同;
參數4是一個創建標識,如果是CREATE_SUSPENDED,則在懸掛狀態創建線程,在線程創建後線程掛起,否則線程在創建後開始線程的執行。
參數5表示線程的安全屬性,NT下有用。
工作者線程的AfxBeginThread
工作者線程的AfxBeginThread的原型如下:
CWinThread* AFXAPI AfxBeginThread(
AFX_THREADPROC pfnThreadProc,
LPVOID pParam,
int nPriority,
UINT nStackSize,
DWORD dwCreateFlags,
LPSECURITY_ATTRIBUTES lpSecurityAttrs)
其中:
參數1指定控制函數的地址;
參數2指定傳遞給控制函數的參數;
參數3、4、5分別指定線程的優先級、堆棧大小、創建標識、安全屬性,含義同用戶界面線程。
AfxBeginThread創建線程的流程
不論哪個AfxBeginThread,首先都是創建MFC線程對象,然後創建Win32線程對象。在創建MFC線程對象時,用戶界面線程和工作者線程的創建分別調用了不同的構造函數。用戶界面線程是從CWinThread派生的,所以,要先調用派生類的缺省構造函數,然後調用CWinThread的缺省構造函數。圖8-1中兩個構造函數所調用的CommonConstruct是MFC內部使用的成員函數。
-
-
- CreateThread和_AfxThreadEntry
MFC使用CWinThread::CreateThread創建線程,不論對工作者線程或用戶界面線程,都指定線程的入口函數是_AfxThreadEntry。_AfxThreadEntry調用AfxInitThread初始化線程。
CreateThread和_AfxThreadEntry在線程的創建過程中使用同步手段交互等待、執行。CreateThread由創建線程執行,_AfxThreadEntry由被創建的線程執行,兩者通過兩個事件對象(hEvent和hEvent2)同步:
在創建了新線程之後,創建線程將在hEvent事件上無限等待直到新線程給出創建結果;新線程在創建成功或者失敗之後,觸發事件hEvent讓父線程運行,並且在hEven2上無限等待直到父線程退出CreateThread函數;父線程(創建線程)因爲hEvent的置位結束等待,繼續執行,退出CreateThread之前觸發hEvent2事件;新線程(子線程)因爲hEvent2的置位結束等待,開始執行控制函數(工作者線程)或者進入消息循環(用戶界面線程)。
MFC在線程創建中使用瞭如下數據結構:
struct _AFX_THREAD_STARTUP
{
//傳遞給線程啓動的參數(IN)
_AFX_THREAD_STATE* pThreadState;//父線程的線程狀態
CWinThread* pThread; //新創建的MFC線程對象
DWORD dwCreateFlags; //線程創建標識
_PNH pfnNewHandler; //新線程的句柄
HANDLE hEvent; //同步事件,線程創建成功或失敗後置位
HANDLE hEvent2; //同步事件,新線程恢復執行後置位
//返回給創建線程的參數,在新線程恢復執行後賦值
BOOL bError; //如果創建發生錯誤,TRUE
};
該結構作爲線程開始函數的參數被傳遞給_beginthreadex函數來創建和啓動線程。_beginthreadex函數是“C”的線程創建函數,具有如下原型:
unsigned long _beginthreadex(
void *security,
unsigned stack_size,
unsigned ( __stdcall *start_address )( void * ),
void *arglist,
unsigned initflag,
unsigned *thrdaddr );
圖8-2描述了上述過程。圖中表示,_AfxThreadEntry在啓動線程時,將創建本線程的線程狀態,並且繼承父線程的模塊狀態。關於MFC狀態,見第9章。
- 線程的結束
從圖8-2可以看出,AfxEndThread用來結束調用它的線程:它將清理本線程創建的MFC對象和釋放線程局部存儲分配的內存空間;調用CWinThread的虛擬函數Delete;調用“C”的結束線程函數_endthreadex釋放分配給線程的資源,但是不關閉線程句柄。
CWinThread::Delete的缺省實現是:如果本線程的成員函數m_bDelete爲TRUE,則調用“C”運算符號delete銷燬MFC線程對象自身(delete this),這將導致線程對象的析構函數被調用。若析構函數檢測線程句柄非空則調用CloseHandle關閉它。
通常,讓m_bDelete爲TRUE以便自動地銷燬線程對象,釋放內存空間(MFC內存對象在堆中分配)。但是,有時候,在線程結束之後(Win32線程已經不存在)保留MFC線程對象是有用的,當然程序員自己最後要記得銷燬該線程對象。
- 實現線程的消息循環
在MFC中,消息循環是由線程完成的。一般地,可以使用MFC缺省的消息循環(即使用函數CWindThrad::Run),但是,有些時候需要程序員自己實現一個線程的消息循環,比如在用戶界面線程進行一個長時間計算處理或者等待另一個線程時。一般有如下形式:
while ( bDoingBackgroundProcessing)
{
MSG msg;
while ( ::PeekMessage( &msg, NULL,0, 0, PM_NOREMOVE ) )
{
if ( !PumpMessage( ) )
{
bDoingBackgroundProcessing = FALSE;
::PostQuitMessage( );
break;
}
}
// let MFC do its idle processing
LONG lIdle = 0;
while ( AfxGetApp()->OnIdle(lIdle++ ) );
// Perform some background processing here
// using another call to OnIdle
}
該段代碼的解釋參見圖5-3對線程的Run函數的圖解。
程序員實現線程的消息循環有兩個好處,一是顧及了MFC的Idle處理機制;二是在長時間的處理中可以響應用戶產生的事件或者消息。
在同步對象上等待其他線程時,也可以使用同樣的方式,只要把條件
bDoingBackgroundProcessing
換成如下形式:
WaitForSingObject(hHandleOfEvent,0) == WAIT_TIMEOUT
即可。
MFC處理線程和進程時還引入了一個重要的概念:狀態,如線程狀態(Thread State)、進程狀態(Process State)、模塊狀態(Module State)等。由於這個概念在MFC中佔有重要地位,涉及的內容比較多,所以專門在下一章來講述它。