Win32的進程和線程概念
進程是一個可執行的程序,由私有虛擬地址空間、代碼、數據和其他操作系統資源(如進程創建的文件、管道、同步對象等)組成。一個應用程序可以有一個或多個進程,一個進程可以有一個或多個線程,其中一個是主線程。
線程是操作系統分時調度分配CPU時間的基本實體。一個線程可以執行程序的任意部分的代碼,即使這部分代碼被另一個線程並發地執行;一個進程的所有線程共享它的虛擬地址空間、全局變量和操作系統資源。
之所以有線程這個概念,是因為以線程而不是進程為調度對象效率更高:
由於創建新進程必須加載代碼,而線程要執行的代碼已經被映射到進程的地址空間,所以創建、執行線程的速度比進程更快。
一個進程的所有線程共享進程的地址空間和全局變量,所以簡化了線程之間的通訊。
Win32的進程處理簡介
因為MFC沒有提供類處理進程,所以直接使用了Win32 API函數。
進程的創建
調用CreateProcess函數創建新的進程,運行指定的程序。CreateProcess的原型如下:
BOOL CreateProcess(
LPCTSTR lpApplicationName,
LPTSTR lpCommandLine,
LPSECURITY_ATTRIBUTES lpProcessAttributes,
LPSECURITY_ATTRIBUTES lpThreadAttributes,
BOOL bInheritHandles,
DWORD dwCreationFlags,
LPVOID lpEnvironment,
LPCTSTR lpCurrentDirectory,
LPSTARTUPINFO lpStartupInfo,
LPPROCESS_INFORMATION lpProcessInformation
);
其中:
lpApplicationName指向包含了要運行模塊名字的字符串。
lpCommandLine指向命令行字符串。
lpProcessAttributes描述進程的安全性屬性,NT下有用。
lpThreadAttributes描述進程初始線程(主線程)的安全性屬性,NT下有用。
bInHeritHandles表示子進程(被創建的進程)是否可以繼承父進程的句柄。可以繼承的句柄有線程句柄、有名或無名管道、互斥對象、事件、信號量、映像文件、普通文件和通訊端口等;還有一些句柄不能被繼承,如內存句柄、DLL實例句柄、GDI句柄、URER句柄等等。
子進程繼承的句柄由父進程通過命令行方式或者進程間通訊(IPC)方式由父進程傳遞給它。
dwCreationFlags表示創建進程的優先級類別和進程的類型。創建進程的類型分控制台進程、調試進程等;優先級類別用來控制進程的優先級別,分Idle、Normal、High、Real_time四個類別。
lpEnviroment指向環境變量塊,環境變量可以被子進程繼承。
lpCurrentDirectory指向表示當前目錄的字符串,當前目錄可以繼承。
lpStartupInfo指向StartupInfo結構,控制進程的主窗口的出現方式。
lpProcessInformation指向PROCESS_INFORMATION結構,用來存儲返回的進程信息。
從其參數可以看出創建一個新的進程需要指定什麼信息。
從上面的解釋可以看出,一個進程包含了很多信息。若進程創建成功的話,返回一個進程信息結構類型的指針。進程信息結構如下:
typedef struct _PROCESS_INFORMATION {
HANDLE hProcess;
HANDLE hThread;
DWORD dwProcessId;
DWORD dwThreadId;
}PROCESS_INFORMATION;
進程信息結構包括進程句柄,主線程句柄,進程ID,主線程ID。
進程的終止
進程在以下情況下終止:
調用ExitProcess結束進程;
進程的主線程返回,隱含地調用ExitProcess導致進程結束;
進程的最後一個線程終止;
調用TerminateProcess終止進程。
當要結束一個GDI進程時,發送WM_QUIT消息給主窗口,當然也可以從它的任一線程調用ExitProcess。
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位的參數。---www.bianceng.cn
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中占有重要地位,涉及的內容比較多,所以專門在下一章來講述它。