MFC的程序和線程
程序是一個可執行的程式,由私有虛拟位址空間、代碼、資料和其他作業系統資源(如程序建立的檔案、管道、同步對象等)組成。一個應用程式可以有一個或多個程序,一個程序可以有一個或多個線程,其中一個是主線程。
線程是作業系統分時排程配置設定 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(
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
當要存取儲存的資料時,使用索引得到資料指針。
BOOL TlsFree(
DWORD dwTlsIndex // TLS index to free
當每一個線程都不再使用局部存儲資料時,線程釋放它配置設定的動态記憶體。在 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
函數參數包括同步對象的句柄,等待時間,是等待一個還是多個同步對象等等。
一個或全部同步對象獲得信号時傳回(在參數中指定是等待一個或多個同步對象);
可以發出提示的函數 (ALTERABLE)
這些函數主要用于重疊 (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(建立後挂起),則建立的新線程開始執行。
雖然程式員沒有從 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的原型如下:
AFX_THREADPROC pfnThreadProc,
LPVOID pParam,
參數 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中占有重要地位,涉及的内容比較多,是以專門在下一章來講述它。