Windows多線程程序設計
一. 結束線程:
可以利用GetExitCodeThread函數,該函數會傳回線程函數的返回值,然而該函數的一個糟糕行爲是:當線程還在進行,尚未有所謂結束代碼時,它會傳回TRUE表示成功,如果這樣第二個形參lpExitCode指向的內存區域中應該放的是STILL_ACTIVE,要注意這種行爲,也就是說你不可能從其返回值中知道“到底線程還在運行還是它已結束”,而應根據lpExitCode中是否爲STILL_ACTIVE來判斷。
For(;;)
{bool rc; rc = GetExitCodeThread(HANDLE,lpExitCode);
If(rc&&(*lpExitCode)!= STILL_ACTIVE)
//線程結束}
強制結束一個線程可以利用函數void ExitThread(DWORD dwExitCode);形參指定此線程之結束代碼,此函數類似於c runtime library中的exit()函數,因爲它可以在任何時候被調用並且絕不會返回,任何代碼若放在此行之下,保證不會被執行。
程序啓動後就執行的那個線程稱爲主線程,主線程有兩個特點,第一,它必須負責GUI程序中的主消息循環;第二,這一線程的結束(不論是因爲返回或因爲調用了ExitThread)會使得程序中的所有線程都被強迫結束,程序也因此而結束,其他線程沒有機會做清理工作。所以在main或winmain結束之前,應先等待所有的線程都結束。
診斷宏:
#pragma comment( lib, "USER32" )
#include <crtdbg.h>
#define MTASSERT(a) _ASSERTE(a)
#define MTVERIFY(a) if (!(a)) PrintError(#a,__FILE__,__LINE__,GetLastError())
__inline void PrintError(LPSTR linedesc, LPSTR filename, int lineno, DWORD errnum)
{
LPSTR lpBuffer;
char errbuf[256];
#ifdef _WINDOWS
char modulename[MAX_PATH];
#else // _WINDOWS
DWORD numread;
#endif // _WINDOWS
FormatMessage( FORMAT_MESSAGE_ALLOCATE_BUFFER
| FORMAT_MESSAGE_FROM_SYSTEM,
NULL,
errnum,
LANG_NEUTRAL,
(LPTSTR)&lpBuffer,
0,
NULL );
wsprintf(errbuf, "\nThe following call failed at line %d in %s:\n\n"
" %s\n\nReason: %s\n", lineno, filename, linedesc, lpBuffer);
#ifndef _WINDOWS
WriteFile(GetStdHandle(STD_ERROR_HANDLE), errbuf, strlen(errbuf), &numread, FALSE );
Sleep(3000);
#else
GetModuleFileName(NULL, modulename, MAX_PATH);
MessageBox(NULL, errbuf, modulename, MB_ICONWARNING|MB_OK|MB_TASKMODAL|MB_SETFOREGROUND);
#endif
exit(EXIT_FAILURE);
}
多線程程序設計成功的關鍵:
(1) 各線程的數據要分離開來,避免使用全局變量。
(2) 不要在線程之間共享GDI對象
(3) 確定你知道你的線程狀態,不要徑自結束程序而不等待它們的結束。
(4) 讓主線程處理用戶界面。
二.關於Wait…()函數
DWORD WaitForSingleObject(
HANDLE hHandle;
DWORD dwMilliseconds);
參數:
hHandle---等待對象的handle(代表一個核心對象)
dwMilliseconds—等待的最長時間,時間終了,即使handle尚未稱爲激發狀態,此函數還是要返回,此值可以是0(代表立刻返回),也可以是INFINITE代表無窮等待。
返回值:
如果函數失敗,則傳回WAIT_FAILED,這時候你可調用GetLastError取得更多信息,此函數的成功有三個因素:
1. 等待的目標(核心對象)變成激發狀態,這種情況下返回值將爲WAIT_OBJECT_0.
2. 核心對象變成激發狀態之前,等待時間終了,這種情況下返回WAIT_TIMEOUT.
3. 如果一個擁有mutex(互斥器)的線程結束前沒有釋放mutex,則傳回WAIT_ABANDONED.
獲得一個線程對象的handle之後,WaitForSingleObject要求操作系統讓線程1睡覺,直到以下任何一種情況發生:
1. 線程2結束
2. dwMilliseconds時間終了,該值系從函數調用後開始計算。
由於操作系統追蹤線程2,所以即使線程2失事或被強迫終止,該函數也能正常工作。
關於該函數的第二個參數,若設定爲0,可使你能夠檢查handle的狀態並立刻返回,沒有片刻停留,如果handle已經備妥,那麼這個函數會成功並傳回WAIT_OBJECT_0,否則,這個函數立刻返回並傳回WAIT_TIMEOUT.
可被WaitForSingleObject使用的核心對象有兩種狀態:激發與未激發。Wait函數會在目標變成激發狀態時返回。
當線程正在執行時,線程對象處於未激發狀態,當線程結束,線程對象就被激發了,因此,任何線程如果等待的是一個線程對象,將會在等待對象結束時被調用,因爲當時線程對象自動變成激發狀態。
Win32的核心對象激發狀態的意義
對象 |
說明 |
Thread |
當線程結束時,線程對象即被激發,當線程還在進行時,則對象處於未激發狀態,線程對象由CreateThread或CreateRemoteThread產生 |
Process |
當進程結束時,進程對象即被激發,當進程還在進行時,則對象處於未激發狀態,CreateProcess或OpenProcess會傳回一個進程對象的handle |
Change Notification |
當一個特定的磁盤子目錄中發生一件特別的變化時,此對象即被激發,此對象系由FindFirstChangeNotification產生 |
Console Input |
當console窗口的輸入緩衝區中有數據可用時,此對象處於激發狀態,CreateFile或GetStdFile兩函數可以獲得console handle. |
Event |
Event對象的狀態直接受控於應用程序所使用的三個Win32函數:SetEvent(), PulseEvent,RestEvent。CreateEvent或OpenEvent都可以傳回一個handle,Event對象的狀態可被操作系統設定---如果使用於overlapped操作時。 |
Mutex |
如果mutex沒有被任何線程所擁有,它就是處於激發狀態,一旦一個等待mutex的函數返回了,mutex也就自動重置爲未激發狀態,CreateMutex或OpenMutex都可以獲得一個mutex handle。 |
Semaphore |
Semaphore有點像mutex,但它有個計數器,可以約束其擁有者(線程)的個數,當計數器大於0時,Semaphore處於激發狀態,當計數器等於0時,semaphore處於未激發狀態,CreateSemaphore或OpenSemaphore可以傳回一個semaphore handle。 |
WaitForMultipleObject函數:
DWORD WaitForMultipleObject(
DWORD nCount,
CONST HANDLE *lpHandles,
BOOL bWaitAll,
DWORD dwMillisencods)
參數:
nCount—表示lpHandles所指之handles數組的元素個數,最大容量爲MAXIMUM_WAIT_OBJECTS.
lpHandles—指向一個由對象handles所組成的數組,這些handles不需要爲相同的類型。
bWaitAll—如果此爲true,表示所有的handles都必須激發,此函數才得以返回,否則此函數將在任何一個handle激發時返回。
dwMilliseconds—當該事件長度終了時,即使沒有任何handles激發,此函數也會返回,此值可爲0,以便測試,亦可指定INFINITE,表示無窮等待。
返回值:
1. 如果因時間終了而返回,則返回值是WAIT_TIMEOUT.
2. 如果bWaitAll是TRUE,那麼返回值將是WAIT_OBJECT_0.
3. 如果bWaitAll是FALSE,那麼返回值減去WAIT_OBJECT_0,就表示數組中的哪一個handle被激發了。
4. 如果你等待的對象中有任何mutexes,那麼返回值可能從WAIT_ABANDONED_0到WAIT_ABANDONED_0+nCount-1.
5. 如果函數失敗,它會傳回WAIT_FAILED,這時候你可以用GetLastError找出失敗的原因。
GetMessage函數等待消息而不是核心對象,一旦你調用GetMessage,除非有一個消息真正進入你的消息隊列,否則它不會返回,在此期間,Windows就可以自由地將CPU時間給與其他程序。
如果你正使用WaitSingleObject或WaitForMultipleObjects等待某個對象被激發,你根本沒有辦法回到主消息循環中去。爲解決這個問題,主消息循環必須修改,使它得以同時等待消息或核心對象被激發,必須使用一個MsgWaitForMultipleObjects函數,這個函數非常類似WaitForMultipleObjects,但它會在“對象被激發”或“消息到達隊列”時被喚醒而返回。
DWORD MsgWaitForMultipleObjects(
DWORD nCount,
LPHANDLE lpHandles,
BOOL fWaitAll,
DWORD dwMilliseconds,
DWORD dwWakeMask);
參數:
dwWakeMask—欲觀察的用戶輸入消息,可以是: QS_ALLINPUT,QS_HOTKEY,QS_INPUT,QS_KEY,QS_MOUSE, QS_MOUSEBUTTON,QS_MOUSEMOVE,QS_PAINT,QS_POSTMESSAGE,QS_SENDMESSAGE,QS_TIMER.
返回值:
和WaitForMultipleObjects相比較,MsgWaitForMultipleObjects有一些額外的返回值意義,爲了表示“消息到達隊列”,返回值將是WAIT_OBJECT_0+nCount。
while(!quit || gNumPrinting > 0)
{
DWORD dwWake;
dwWake = MsgWaitForMultipleObjects(gNumPrintings,gPrintJobs,FALSE,INFINITE,QS_ALLEVENTS);
if(dwWake >= WAIT_OBJECT_0 && dwWake < WAIT_OBJECT_OBJECT_0+gNumPrinting)
{
//處理有信號的核心對象
}
else if(dwWake == WAIT_OBJECT_0 + gNumPrinting)
{
While(PeekMessage(&msg,NULL,0,0,PM_REMOVE))
{
If(hDlgMain == NULL || !IsDialogMessage(hDlgMain,&msg))
{
if(msg.message == WM_QUIT)
{
quit = TRUE;
exitcode = msg.wParam;
break;
}
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}//end while
}
}// end while
有數種情況是這個循環必須處理而卻可能在它第一次設計時容易被忽略的:
1. 在你收到WM_QUIT之後,Windows仍然會傳送消息給你,如果你要在收到WM_QUIT之後等待所有線程結束,你必須繼續處理你的消息,否則窗口會變得反應遲鈍,而且沒有重繪能力。
2. 該函數不允許handles數組中有縫隙產生。所以當某個handle被激發了時,你應該在下一次調用該函數之前先把handles數組做個整理,緊壓,不要只是把數組中的handle設爲NULL。
3. 如果有另一個線程更改了對象數組,而那是你正在等待的,那麼你需要一種新方法,可以強迫MsgWaitForMultipleObjects返回,並重新開始,以包含這個新的handle。
三. 同步控制
當線程1調用線程2時,線程1停下不動,直到線程2完成返回到線程1來,線程1才繼續下去,這就是所謂的同步(synchronous),如果線程1調用線程2後,徑自繼續自己的下一個動作,那麼兩者之間就是所謂的異步(asynchronous),例如SendMessage就是同步行爲,而PostMessage屬於異步行爲。
1. 臨界區:critical section並不是核心對象,因此,沒有所謂的handle這樣的東西,它和核心對象不同,它存在於進程的內存空間中,你不需要使用像“create”這樣的api函數來獲得一個critical section handle,你應該做的是將一個類型爲CRITICAL_SECTION的局部變量初始化,方法是調用InitializeCriticalSection;
VOID IniitializeCriticalSection(
LPCRITICAL_SECTION lpCriticalSection),當利用畢cirtical section時,你必須調用DeleteCriticalSection清除它,但這個函數並沒有“釋放對象”的意義在裏頭。一旦critical section被初始化,每一個線程就可以進入其中—只要它通過了EnterCriticalSection這一關。當線程準備好離開cirtical section時,它必須調用LeaveCriticalSection。一旦一個線程進入一個critical section,它就能夠一再地重複進入該critical section,但是每一個“進入”操作都必須有一個對應的“離開”操作。
千萬不要在critical section之中調用Sleep或任何的Wait..函數。由於critical section不是核心對象,如果進入critical section的那個線程結束了或當掉了,而沒有調用LeaveCriticalSection的話,系統沒有辦法將critical section清除,如果你需要那樣的機能,你應該使用mutex。在Windows NT之中,如果一個線程進入某個critical section而在未離開的情況下就結束,該critical section會被永遠鎖住,而在Windows95中,如果發生同樣的情況,其他等着要進入該cirtical section的線程,將獲准進入,這基本上是一個嚴重的問題,因爲你竟然可以在你的程序處於不穩定狀態時進入該critical section。
2. 互斥體(mutex):mutex和critical section做相同的事情,但是它們的運作還是有差別的。(1)鎖住一個未被擁有的mutex,比鎖住一個未被擁有的critical section,需要花費幾乎100倍的時間,因爲critical section不需要進入操作系統的內核,直接在ring3級就可以進行操作。(2)Mutexes可以跨進程使用,Critical section則只能在同一個進程中使用。(3)等待一個mutex時,你可以指定“結束等待”的時間長度,但對於critical section則不行。
爲了能夠跨進程使用同一個mutex,你可以在產生mutex時指定其名稱,這個名稱對整個系統而言是全局性的,所以應保證其獨一無二性,如果你指定了名稱,系統中的其他任何線程就可以使用這個名稱來處理該mutex,一定要使用名稱,因爲你沒有辦法把handle交給一個執行中的進程。
Mutex是一個核心對象,因此它被保持在系統核心之中,並且和其他核心對象一樣,有所謂的引用計數。利用CreateMutex函數產生一個mutex。HANDLE CreateMutex(
LPSECURITY_ATTRIBUTES lpMutexAttributes,
BOOL bInitialOwner,
LPCTSTR lpName)
lpMutexAttributes—安全屬性,NULL表示使用默認的屬性,這一指定在Win95中無效。
bInitialOwner—如果你希望“調用CreateMutex的這個線程”擁有產生出來的mutex,就將此值設爲TRUE.
lpName—mutex的名稱,任何進程或線程都可以根據此名稱使用這一mutex,名稱可以是任意字符串,只要不含\即可。
返回值:如果成功,則傳回mutex的handle,否則傳回NULL,調用GetLastError可以獲得進一步的信息,如果指定mutex的名稱已經存在,GetLastError會傳回ERROR_ALREADY_EXISTS.
當不再需要一個mutex時,可調用CloseHandle將它關閉,和其他核心對象一樣,mutex有一個引用計數,每次調用CloseHandle,引用計數便減1,當引用計數爲0時,mutex便自動被系統銷燬。
打開一個應經存在的mutex,可調用函數OpenMutex,如調用CreateMutex建立一個已經存在的mutex,會傳回該mutex handle,但是GetLastError會傳回ERROR_ALREADY_EXISTS.
欲獲得一個mutex的擁有權,使用Win32的Wait。。。()函數,如果沒有任何線程擁有那個mutex,Wait。。函數就會成功。Mutex的擁有權並非屬於那個產生它的線程,而是那個最後對此Mutex進行Wait。。。操作並且尚未進行ReleaseMutex操作的線程。
在一個適當的程序中,線程絕對不應該在它即將結束前還擁有一個mutex,因爲這意味着線程沒有能夠適當地清除其資源,爲了解決這個問題,mutex有一個非常重要的特性,這性質在各種同步機制中是獨一無二的。如果線程擁有一個mutex而在結束前沒有調用ReleaseMutex,mutex不會被摧毀,取而代之,該mutex會被視爲“未被擁有”以及“未被激發”,而下一個等待中的線程會被以WAIT_ABANDONED_0通知,不論線程是因爲ExitThread而結束,或是因當掉而結束,這種情況都存在。如果其他線程正以WaitForMultipleObjects等待此mutex,該函數也會返回,傳回值介於WAIT_OBJECT_0和WAIT_OBJECT_n+1之間,其中n爲handle數組的元素個數,線程可以根據這個值瞭解到究竟哪一個mutex被放棄了,至於WaitForSingleObject則只是傳回WAIT_ABANDONED_0.
CreateMutex的第二個參數允許你指定現行線程是否立刻擁有即將產生出來的mutex。是爲了阻止跨進程使用時導致的race condition的發生。
3. 要在Win32環境中產生一個semaphore,必須使用CreateSemaphore函數。
HANDLE CreateSemaphore(
LPSECURITY_ATTRIBUTES lpAttributes,
LONG lInitialCount,
LONG lMaximumCount,
LpCTSTR lpName)
參數:
lpAttributes----安全屬性,如果是NULL,就表示要使用默認屬性,Win95忽略這一參數。
lInitialCount---semaphore的初值,必須大於或等於0,並且小於或等於lMaximumCount。
lMaximumCount---semaphore的最大值,這也就是在同一時間內能夠鎖住semaphore之線程的最多個數。
lpName---semaphore的名稱,任何線程或進程都可以根據這一名稱引用到這個semaphore,這個值可以是NULL,意思是產生一個沒有名字的semaphore。
返回值:
如果成功就傳回一個handle,否則傳回NULL,不論哪一種情況GetLastError都會傳回一個合理的結果,如果指定的semaphore名稱已經存在,則該函數還是成功,GetLastError會傳回ERROR_ALREADY_EXISTS。
如果鎖定成功(利用wait函數),與mutex不同的是,你並不會收到semaphore的擁有權,因爲可以有一個以上的線程同時鎖定一個semaphore,所以談semaphore的擁有權並沒有太多的意義。因爲沒有擁有權這種概念,一個線程可以反覆調用Wait函數以產生新的鎖定,這和mutex不同,擁有mutex的線程不論再調用多少次wait函數,也不會被阻塞住。
BOOL ReleaseSemaphore(HANDLE hSemaphore,LONG lReleaseCount,LPLONG lpPreviousCount)
參數:
hSemaphore----Semaphore的handle
lReleaseCount—Semaphore現值的增額,該值不可以是負值或0
lpPreviousCount—藉此傳回semaphore原來的值。
返回值:
如果成功,則傳回TRUE,否則傳回FALSE,失敗時可調用GetLastError獲得原因。
ReleaseSemaphore對於semaphore所造成的現值的增加絕對不會超過CreateSemaphore時所指定的lMaximumCount。注意lpPreviousCount所傳回來的是一個瞬間值,你不可以把lReleaseCount加上*lpPreviousCount就當做是semaphore的現值,因爲其他線程可能已經改變了semaphore的值。同時與mutex不同的是,調用ReleaseSemaphore的那個線程並不一定就得是調用wait的那個線程,任何線程都可以在任何時間調用ReleaseSemaphore,解除被任何線程鎖定的semaphore。CreateSemaphore的第二個參數lInitialCount,它的存在理由和CreateMutex的bInitialOwner參數的存在理由是一樣的,如果你把初值設定爲0,你的線程就可以在產生semaphore之後進行所有必要的初始化工作,待初始化工作完成之後,調用ReleaseSemaphore就可以把現值增加到其最大可能值。
4. EVENT.Event是核心對象,它的唯一目的就是成爲激發狀態或未激發狀態,這兩種狀態全由程序來控制。爲了產生一個event對象,你必須調用CreateEvent函數。
HANDLE CreateEvent(
LPSECURITY_ATTRIBUTES lpEventAttributes,
BOOL bManualReset,
BOOL bInitialState,LPCTSTR lpName)
參數:
lpEventAttributes---安全屬性,NULL表示默認屬性,該屬性在Win95中會被忽略。
bManualReset---如爲FALSE,表示這個event將在變成激發狀態(因而喚醒一個線程)之後,自動重置爲非激發狀態,如果是TRUE,表示不會自動重置,必須靠程序操作(調用ResetEvent)才能將激發狀態的event重置爲非激發狀態。
bInitialState—如爲TRUE,表示這個event一開始處於激發狀態,如爲FALSE,則表示這個event一開始處於非激發狀態。
lpName---event對象的名字,任何線程或進程都可以根據這個文字名稱,使用這一event對象。
返回值:
如果調用成功,會傳回一個handle,如果lpName指定的event對象已經存在,傳回的是該event的handle,而不會產生一個新的event,這時候GetLastError會傳回ERROR_ALREADY_EXISTS.
常用的操作event的函數及其說明如下:
函數 |
說明 |
SetEvent |
把event對象設爲激發狀態 |
ResetEvent |
把event對象設爲非激發狀態 |
PulseEvent |
如果是一個Manual Reset Event:把event對象設爲激發狀態,喚醒“所有”等待中的線程,然後event恢復爲非激發狀態。如果是一個Auto Reset Event:把event對象設爲激發狀態,喚醒“一個”等待中的線程,然後event恢復爲非激發狀態。 |
5. Interlocked函數:InterlockedIncrement和InterlockedDecrement函數,這個兩個函數的返回值只能夠和0做比較,不能和任何其他數值比較。另外InterlockedExchange函數可以設定一個新值並傳回舊值,它提供了一個在多線程環境下的安全做法,用以完成一個很基礎的運算操作。
6. 同步機制總結:
Critical Section用來實現“排他性佔有”,適用範圍是單一進程的各線程之間,它是一個局部對象,不是一個核心對象;快速而有效率;不能夠同時有一個以上的critical section被等待;無法偵測是否已被某個線程放棄。
Mutex是一個核心對象,可以在不同的線程之間實現“排他性佔有”,甚至即使那些線程分屬不同進程,它是一個核心對象;如果擁有mutex的那個線程結束,則會產生一個“abandoned”錯誤信息;可以使用Wait…函數等待一個mutex;可以具名,因此可以被其他進程開啓;只能夠被擁有它的那個線程釋放
Semaphore被用來追蹤有限的資源,它是一個核心對象;沒有擁有者;可以具名,因此可以被其他進程開啓;可以被任何一個線程釋放。
Event通常用於overlapped I/O,或用來設計某些自定義的同步對象,它是一個核心對象;完全在程序掌控之下;適用於設計新的同步對象;“要求甦醒”的請求並不會被儲存起來,可能會遺失掉;可以具名,因此可以被其他進程開啓。
四. 線程的初始化結束以及優先級的調整
1. 結束一個線程
BOOL TerminateThread(HANDLE hThread,DWORD dwExitCode)
參數:
hThread---欲令其結束的線程的handle,該線程是我們的行動目標
dwExitCode—該線程的結束代碼
返回值:如果函數成功,則傳回TRUE,如果失敗,則傳回FALSE,GetLastError可獲知更多細節。
TerminateThread強迫一個線程結束,手段激烈而有力,甚至不允許該線程有任何掙扎的機會,所以該線程沒有機會在結束前清理自己,該函數不會再目標線程中丟出一個異常情況,目標線程在覈心層面就被根本抹殺了,目標線程沒有機會捕捉所謂的“結束請求”,並從而獲得清理自己的機會。而且目標線程的堆棧沒有被釋放掉,於是可能引起一大塊內存泄露,而且,任何一個與此線程有附着關係的DLLs也都沒有機會獲得“線程解除附着”的通知。這個函數所帶來的潛伏危機還包括:如果線程正進入一個critical section之中,該critical section將因此永遠處於鎖定狀態,因爲critical section不像mutex那樣擁有“abandoned”狀態。所以應儘量避免使用該函數來結束一個線程。推薦的做法是利用一個手動重置的event對象,worker線程可以檢查該event對象的狀態,來決定是否結束自己。
2. 線程優先權
Win32優先權是以數值表現的,並以進程的“優先權類別”、線程的“優先權層級”和操作系統當時採用的“動態提升”作爲計算基準,所有因素放在一起,最後獲得一個0~31的數值。
(1) 優先權類別:是進程的屬性之一,這個屬性可以表現出這一進程和其他進程比較之下的重要性。Win32提供4種優先權類別,每一個優先權類別對應一個基本的優先權層級。優先權類別適用於進程而非線程,操作函數爲SetPriorityClass和GetPriorityClass。
優先權類別(Priority class) |
基礎優先權值 |
HIGH_PRIORITY_CLASS |
13 |
IDLE_PRIORITY_CLASS |
4 |
NORMAL_PRIORITY_CLASS |
7或8 |
REALTIME_PRIORITY_CLASS |
24 |
注意類別REAL_PRIORITY_CLASS,這個類別用以協助解決一些和時間有密切關係的工作,不應該用於標準GUI程序或甚至於典型的服務器程序,可用在監控驅動程序方面。
(2)優先權層級:是對進程的優先權類別的一個修改,使你能夠調整同一個進程內的各線程的相對重要性,共有7種優先權層級。可利用函數SetThreadPriority和GetThreadPriority來調整和獲取優先權層級。
優先權層級(Prioriyt levels) |
調整值 |
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 |
Set to 1 |
THREAD_PRIORITY_TIME_CRITICAL |
Set to 15 |
(3)動態提升:決定線程真正優先權的最後一個因素是其目前的動態提升值,所謂動態提升是對優先權的一種調整,使系統能夠機動對待線程以強化程序的可用性。一般來說,擁有鍵盤焦點的程序的優先權得以提升+2,這個設定使得前臺程序比後臺程序獲得較多的CPU時間,因此即使系統忙碌,前臺程序還是容易保持其UI敏感度。同樣優先權提升也適用於同一個進程的線程,用以反應用戶的輸入或磁盤的輸入。(鼠標消息和計時器消息也引起優先權提升),另外還可能發生在任何一個線程身上,當該線程“等待狀態”獲得滿足時。
3.線程的掛起與喚醒
調用函數SuspendThread,使指定的線程掛起,直到有人調用ResumeThread將其喚醒,若ResumeThread函數調用成功,返回線程的前一個掛起次數,失敗則傳回0XFFFFFFFF,可調用GetLastError獲取更多信息。SuspendThread成功返回該線程目前的掛起次數,失敗返回0XFFFFFFFF。SuspendThread的最大用途就是用來協助撰寫調試器,要注意該函數可能引起的死鎖問題。
五、OVERLAPPED I/O
Overlapped I/O是Win32的一種技術,你可以要求操作系統爲你傳送數據,並且在傳送完畢時通知你,這項技術使你的程序在I/O進行過程中仍然能夠繼續處理事務。
(1)Win32文件操作函數
CreateFile可以用來打開各式各樣的資源,包括文件(硬盤,軟盤,光盤或其他),串行口和並行口,Named pipes,console等。
HANDLE CreateFile(
LPCTSTR lpFileName, //指向文件名稱
DWORD dwDesiredAccess,//存取模式(讀或寫)
DWORD dwShareMode,//共享模式
LPSECURITY_ATTRIBUTES lpSecurityAttributes,//指向安全屬性結構
DWORD dwCreationDisposition,//如何產生
DWORD dwFlagsAndAttributes,//文件屬性
HANDLE hTemplateFile //一個臨時文件,將擁有全部的屬性拷貝
)
在該函數的第六個參數中可設置FILE_FLAG_OVERLAPPED與其他值進行組合使用異步調用,Overlapped I/O的基本型式是以ReadFile和WriteFile完成的,這時你不能再調用c runtime library中的stdio.h中的C runtime函數。
BOOL ReadFile(
HANDLE hFile,//欲讀取的文件句柄
LPVOID lpBuffer,//接受數據的緩衝區
DWORD nNumberOfBytesToRead,//欲讀取的字節個數
LPDWORD lpNumberOfBytesRead,//實際讀取的字節個數的地址
LPOVERLAPPED lpOverlapped//指針,指向overlapped info
);
BOOL WriteFile(
HANDLE hFile,//欲寫入的文件句柄
LPVOID lpBuffer,//儲存數據的緩衝區
DWORD nNumberOfBytesToWrite,//欲寫入的字節個數
LPDWORD lpNumberOfBytesWritten,//實際寫入的字節個數的地址
LPOVERLAPPED lpOverlapped//指針,指向overlapped info
);
上述兩個函數類似於C runtime函數中的fread何fwrite,差別在於最後一個lpOverlapped參數,若CreateFile的第6個參數中指定FILE_FLAG_OVERLAPPED,就必須在上述函數中提供一個指向OVERLAPPED結構的指針。
(2)OVERLAPPED結構
OVERLAPPED結構執行兩個功能:一,用以識別每一個目前正在進行的overlapped操作,二,它在你和系統之間提供一個共享區域,參數可以在該區域中雙向傳遞。
typedef struct _OVERLAPPED{
DWORD Internal;
DWORD InternalHigh;
DWORD Offset;
DWORD OffsetHigh;
HANDLE hEvent;
}OVERLAPPED,*LPOVERLAPPED;
成員名稱 |
說明 |
Internal |
通產它被保留,然而當GetOverlappedResult返回FALSE,且GetLastError並非傳回ERROR_IO_PENDING時,這個欄位將內含一個視系統而定的狀態。 |
InternalHigh |
通常它被保留,然而當GetOverlappedResult傳回TRUE時,這個欄位將內含“被傳輸數據的長度”。 |
Offset |
文件中開始被讀取或寫入的偏移位置(以字節爲單位),該偏移位置從文件頭開始算起,如果目標設備(如pipes)並沒有支持文件位置,此欄位被忽略。 |
OffsetHigh |
64位的文件偏移位置中,較高的32位,如果目標設備(如PIPES)並沒有支持文件位置,此欄位被忽略。 |
hEvent |
一個手動重置的event對象,當overlapped I/O完成時即被觸發,ReadFileEx和WriteFileEx會忽略這個欄位,彼時它可能被用來傳遞一個用戶自定義的指針。 |
由於OVERLAPPED結構的生命期超越ReadFile和WriteFile函數,因此該結構最好放在heap中。
(3)被激發的File Handles
如果你需要等待overlapped I/O的執行結果,可利用WaitForMultipleObjects函數,因爲文件handle是核心對象,一旦操作完畢即被激發。當你操作完成之後,請調用GetOverlappedResult以確定結果如何。這個函數的價值在於,在文件操作真正完成之前,你不可能確實知道它是否成功。
BOOL GetOverlappedResult(
HANDLE hFile,
LPOVERLAPPED lpOverlapped,
LPDWORD lpNumberOfBytesTransferred,
BOOL bWait);
參數:
hFile----文件或設備的handle。
lpOverlapped----一個指針,指向OVERLAPPED結構。
lpNumberOfBytesTransferred----一個指針,指向用以表示真正被傳輸的字節個數。
bWait---用以表示是否要等待操作完成。TRUE表示要等待。
返回值:若操作成功,返回TRUE,否則返回FALSE,可用GetLastError獲取詳細信息。
雖然你要求一個overlapped操作,但它不一定就是overlapped,如果數據已經被放進cache中,或操作系統認爲它可以很快地取得那份數據,那麼文件操作就會在ReadFile或WriteFile返回之前完成,而這兩個函數將返回TRUE,這種情況下,文件handle處於激發狀態,而對文件的操作可被視爲overlaped一樣。另外,如果你要求一個文件操作爲overlapped,而操作系統把這個“操作請求”放到隊列中等待執行,那麼ReadFile和WriteFile都會返回FALSE,這時你必須調用GetLastError並確定它傳回ERROR_IO_PENDING,那意味着“overlapped I/O請求”被放進隊列中等待執行,若返回ERROR_HANDLE_EOF,那就真正代表一個錯誤了。
(4)被激發的Event對象
以handle作爲激發機制,一個明顯的限制就是無法說出到底是哪一個overlapped操作完成了,若爲每一個可能正在進行中的overlapped操作調用GetOverlappedResult,則效率很低,這時可利用OVERLAPPED結構體中的hEvent成員。注意該event對象必須是一個手動重置的,若爲自動重置,系統核心可能在你有機會等待event對象之前,先激發它,而event對象的狀態時不能被保存的,所以可能導致event狀態遺失,從而使得Wait函數永不返回。
調用ReadFile時,由於操作系統的原因,爲overlapped I/O指定的緩衝區必須在內存中鎖定,如果系統或同一個程序中有太多緩衝區在同一時間鎖定,Win32可能會傳回ERROR_INVALID_USER_BUFFER,代表此刻沒有足夠的資源來處理這個“I/O請求”,此時可讓線程sleep一段時間,然後再重複調用ReadFile。
(5)異步過程調用(APCs)
使用OVERLAPPED結構中的event對象,只能等待MAXIMUM_WAIT_OBJECTS個對象,此值一般爲64,另外還要根據哪一個event對象被激發,而調用相應的處理過程,解決的方法就是異步過程調用,即Ex版的ReadFile和WriteFile函數,這兩個函數的最後一個參數是一個回調函數地址,當一個overlapped I/O完成時,系統應該調用該函數,此函數被稱作I/O completion routine。Windows調用該回調函數的時機是:你的線程必須處於所謂的“alertable”狀態下。如果線程因爲以下五個函數而處於等待狀態,而其“alertable”標記爲TRUE,則該線程就處於“alertable”狀態。這五個函數爲:
SleepEx;WaitForSingleObjectEx;WaitForMultipleObjectsEx;MsgWaitForMultipleObjectsEx和SignalObjectAndWait函數。只有當線程處於“alertable”狀態時,APCs纔會被調用!
你提供的I/O completion routine回調函數樣式如下:
VOID WINAPI FileIOCompletionRoution(
DWORD dwErrorCode,
DWORD dwNumberOfBytesTransferred,
LPOVERLAPPED lpOverlapped)
參數:
dwErrorCode---這個參數內含以下的值:0表示操作完成,ERROR_HANDLE表示操作已到文件尾端。
dwNumberOfBytesTransferred----真正被傳輸的字節數。
lpOverlapped----指向OVERLAPPED結構,此結構由開啓overlapped I/O操作的函數提供。
若利用I/O completion routine,OVERLAPPED結構中的hEvent欄位就不必放置一個event handle,而是自由運用。
對於少於32KB的數據傳輸請求,採用overlapped I/O會降低效率,另外有兩種情況overlapped I/O總是會同步執行。一.你進行一個寫入操作而造成文件的擴展;二.你讀寫一個壓縮文件。
(6)I/O Completion Ports
APCs的問題是有些I/O API並不支持APCs,如listen和WaitCommEvent,另外只有發出“overlapped請求”的那個線程才能夠提供callback函數。
I/O Completion Ports是一種特殊的核心對象,用來綜合一堆線程,讓它們爲“overlapped請求”服務,其所提供的功能甚至可以跨越多個CPU,其好處包括:與WaitForMultipleObjects不同,不限制handles的個數;允許一個線程將一個請求暫時保存下來,而由另一個線程爲它做實際服務;默默支持scalable架構(是指藉着RAM或硬盤空間或CPU個數增加而能夠提升應用程序性能的一種系統)。
I/O Completion Ports的操作流程:①產生一個I/O completion port;②讓它和一個文件handle產生關聯;③產生一堆線程;④讓每一個線程在Completion Port上等待;⑤開始對着那個文件handle發出一些overlapped I/O請求。
產生一個I/O Completion Port,調用函數CrateIoCompletionPort(
HANDLE FileHandle,HANDLE ExistingCompletionPort,
DWORD CompletionKey,DWORD NumberOfConcurrentThreads)
參數:
FileHandle----文件或設備的handle,在Windows NT3.51之後,此欄位可設定爲INVALID_HANDLE_VALUE,於是產生一個沒有和任何文件handle有關係的port。
ExistingCompletionPort----如果此欄位被指定,那麼上一欄位FileHandle就會被加到此port之上,而不會產生新的port,指定NULL可以產生一個新的port。
CompletionKey----用戶自定義的一個數值,將被交給提供服務的線程,此值和FileHandle有關聯。
NumberOfConcurrentThread----與此I/O completion port有關聯的線程個數。
返回值:如果函數成功,則傳回一個I/O completion port的handle,如果失敗,則傳回FALSE,GeLastError可獲得更詳細的失敗原因。
任何文件只要附着到一個I/O completion port身上,都必須先以FILE_FLAG_OVERLAPPED開啓,如果已經附着上去,就不能夠再以ReadFileEx和WriteFileEx操作它。通常把NumberOfConcurrentThreads設置爲0。
通常CreateIoCompletionPort被調用兩次,第一次先指定FileHandle爲INVALID_HANDLE_VALUE,並設定ExistingCompletionPort爲NULL,用以產生一個port,然後再爲每一個欲附着上去的文件handle調用一次CreateIoCompletionPort,將ExistingCompletionPort設定爲第一次調用所傳回的handle。
一旦completion port產生出來,必須自己以CreateThread或_beginthreadex或AfxBeginThread產生出線程,產生的合理的線程個數應該是CPU個數的兩倍再加2。在completion port上等待的線程是以先進後出的次序提供服務。
Worker線程初始化自己後,它應該調用GetQueuedCompletionStatus,這個操作像是WaitForSingleObject和GetOverlappedResult的組合,函數如下:
BOOL GetQueuedCompletionStatus(
HANDLE CompletionPort,
LPDWORD lpNumberOfBytesTransferred,
LPDWORD lpCompletionKey,
LPOVERLAPPED* lpOverlapped,
DWORD dwMilliseconds);
參數:
CompletionPort----將在其上等待的completion port。
lpNumberOfBytesTransferred----一個指針,指向DWORD,該DWORD將收到被傳輸的數據字節數。
lpCompletionKey----一個指針,指向DWORD,該DWORD將收到由CreateIoCompletionPort所定義的key。
lpOverlapped----這個欄位的名稱是個錯誤,它其實應該命名爲lplpOverlapped,你應該把一個指針的地址放在上面,系統會填以一個overlapped結構的指針,該結構用以初始化I/O操作。
dwMilliseconds----等待的最長時間(毫秒),如果時間終了,lpOverlapped將被設爲NULL,而函數傳回FALSE。
返回值:
如果函數成功地將一個completion packet從隊列中取出,並完成一個成功的操作,函數將傳回TRUE,並填寫由lpNumberOfBytesTransferred,lpCompletionKey和lpOverlapped所指向的變量內容。
如果操作失敗,但completion packet已經從隊列中取出,則函數傳回FALSE,lpOverlapped指向失敗之操作,調用GetLastError可獲知原因。
如果函數失敗,則傳回FALSE,並將lpOverlapped設爲NULL,調用GetLastError可獲知原因。
下面這些函數調用可以啓動“能夠被一個I/O completion port掌握”的I/O操作。ConnectNamePipe;DeviceIoControl;LockFileEx;ReadFile;TransactNamePipe;WaitCommEvent;WriteFile。爲了使用completion port,主線程或任何其他線程可以對着一個與此completion port有關聯的文件,進行讀,寫或其他操作,該線程不需要調用WaitForMultipleObjects,因爲池子裏的各個線程都曾經調用過GetQueuedCompletionStatus,一個I/O操作完成,一個等待中的線程將會自動被釋放,以服務該操作。
有時文件以overlapped I/O狀態打開,進行簡單的讀寫,當寫入操作完成時,I/O completion port將收到一個packet中,但是我們並不想使每個操作均引發completion port通告。我們可以進行一個I/O操作,卻不引發I/O completion packet被送往completion port,我們可以以舊有的受激發的event對象機制取而代之,爲了這麼做,必須設定一個OVERLAPPED結構,內含一個合法的手動重置的event對象,放在hEvent欄位,然後把該handle的最低位設爲1.
OVERLAPPED overlap;
HANDLE hFile;
char buffer[128];
DWORD dwBytesWritten;
Memset(&overlap,0,sizeof(OVERLAPPED));
Overlap.hEvent = CreateEvent(NULL,TRUE,FALSE,NULL);
Overlap.hEvent = (HANDLE)((DWORD)overlap.hEvent | 0x1);
WriteFile(hFile,buffer,128,&dwBytesWritten,&overlap);
六.數據一致性
typedef struct _RWLock
{
// Handle to a mutex that allows
// a single reader at a time access
// to the reader counter.
HANDLE hMutex;
// Handle to a semaphore that keeps
// the data locked for either the
// readers or the writers.
HANDLE hDataLock;
// The count of the number of readers.
// Can legally be zero or one while
// a writer has the data locked.
int nReaderCount;
} RWLock;
//
// Reader/Writer prototypes
//
BOOL InitRWLock(RWLock *pLock);
BOOL DestroyRWLock(RWLock *pLock);
BOOL AcquireReadLock(RWLock *pLock);
int ReleaseReadLock(RWLock *pLock);
BOOL AcquireWriteLock(RWLock *pLock);
int ReleaseWriteLock(RWLock *pLock);
BOOL ReadOK(RWLock *pLock);
BOOL WriteOK(RWLock *pLock);
BOOL FatalError(char *s);
#include <stdio.h>
#include <stdlib.h>
#include <windows.h>
#include "ReadWrit.h"
// If we wait more than 2 seconds, then something is probably wrong!
#define MAXIMUM_TIMEOUT 2000
// Here's the pseudocode for what is going on:
//
// Lock for Reader:
// Lock the mutex
// Bump the count of readers
// If this is the first reader, lock the data
// Release the mutex
//
// Unlock for Reader:
// Lock the mutex
// Decrement the count of readers
// If this is the last reader, unlock the data
// Release the mutex
//
// Lock for Writer:
// Lock the data
//
// Unlock for Reader:
// Unlock the data
///////////////////////////////////////////////////////
BOOL MyWaitForSingleObject(HANDLE hObject)
{
DWORD result;
result = WaitForSingleObject(hObject, MAXIMUM_TIMEOUT);
// Comment this out if you want this to be non-fatal
if (result != WAIT_OBJECT_0)
FatalError("MyWaitForSingleObject - Wait failed, you probably forgot to call release!");
return (result == WAIT_OBJECT_0);
}
BOOL InitRWLock(RWLock *pLock)
{
pLock->nReaderCount = 0;
pLock->hDataLock = CreateSemaphore(NULL, 1, 1, NULL);
if (pLock->hDataLock == NULL)
return FALSE;
pLock->hMutex = CreateMutex(NULL, FALSE, NULL);
if (pLock->hMutex == NULL)
{
CloseHandle(pLock->hDataLock);
return FALSE;
}
return TRUE;
}
BOOL DestroyRWLock(RWLock *pLock)
{
DWORD result = WaitForSingleObject(pLock->hDataLock, 0);
if (result == WAIT_TIMEOUT)
return FatalError("DestroyRWLock - Can't destroy object, it's locked!");
CloseHandle(pLock->hMutex);
CloseHandle(pLock->hDataLock);
return TRUE;
}
BOOL AcquireReadLock(RWLock *pLock)
{
BOOL result = TRUE;
if (!MyWaitForSingleObject(pLock->hMutex))
return FALSE;
if(++pLock->nReaderCount == 1)
result = MyWaitForSingleObject(pLock->hDataLock);
ReleaseMutex(pLock->hMutex);
return result;
}
BOOL ReleaseReadLock(RWLock *pLock)
{
int result;
LONG lPrevCount;
if (!MyWaitForSingleObject(pLock->hMutex))
return FALSE;
if (--pLock->nReaderCount == 0)
result = ReleaseSemaphore(pLock->hDataLock, 1, &lPrevCount);
ReleaseMutex(pLock->hMutex);
return result;
}
BOOL AcquireWriteLock(RWLock *pLock)
{
return MyWaitForSingleObject(pLock->hDataLock);
}
BOOL ReleaseWriteLock(RWLock *pLock)
{
int result;
LONG lPrevCount;
result = ReleaseSemaphore(pLock->hDataLock, 1, &lPrevCount);
if (lPrevCount != 0)
FatalError("ReleaseWriteLock - Semaphore was not locked!");
return result;
}
BOOL ReadOK(RWLock *pLock)
{
// This check is not perfect, because we
// do not know for sure if we are one of
// the readers.
return (pLock->nReaderCount > 0);
}
BOOL WriteOK(RWLock *pLock)
{
DWORD result;
// The first reader may be waiting in the mutex,
// but any more than that is an error.
if (pLock->nReaderCount > 1)
return FALSE;
// This check is not perfect, because we
// do not know for sure if this thread was
// the one that had the semaphore locked.
result = WaitForSingleObject(pLock->hDataLock, 0);
if (result == WAIT_TIMEOUT)
return TRUE;
// a count is kept, which was incremented in Wait.
result = ReleaseSemaphore(pLock->hDataLock, 1, NULL);
if (result == FALSE)
FatalError("WriteOK - ReleaseSemaphore failed");
return FALSE;
}
///////////////////////////////////////////////////////
/*
* Error handler
*/
BOOL FatalError(char *s)
{
fprintf(stdout, "%s\n", s);
// Comment out exit() to prevent termination
exit(EXIT_FAILURE);
return FALSE;
}
七.C Run-Time Library
如果寫一個多線程程序,並且不使用MFC,那麼應該總是和多線程版本的C Runtime Library鏈接,並且總是以_beginthreadex和_endthreadex取代CreateThread和ExitThread。_beginthreadex的參數和CreateThread一樣,並且承擔適度的C Runtime library初始化工作。
unsigned long _beginthreadex(
void* security,
unsigned stack_size,
unsigned (_stdcall *start_address)(void*),
void* arglist,
unsigned initflag,
unsigned* thraddr);
參數:
security----相當於CrateThread中的security參數,NULL表示使用默認的安全屬性,Win95會忽略此參數,對應的Win32數據類型是LPSECURITY_ATTRIBUTES.
stack_size----新線程的堆棧大小,單位是字節,對應的Win32數據類型是DWORD.
start_address----線程啓動時所執行的函數,對應的Win32數據類型是LPTHREAD_START_ROUTINE.
arglist----新線程將收到的一個指針,這個指針只是單純地被傳遞過去,runtime library並沒有對它做拷貝操作,對應的Win32數據類型是LPVOID.
initflag----啓動時的狀態標記,對應的Win32數據類型是DWORD.
thrdaddr----新線程的ID將藉此參數傳回,對應的Win32數據類型是LPDWORD.
返回值:傳回線程的handle,此值必須被強制轉換爲Win32的HANDLE後才能使用,如果函數失敗,傳回0,而其原因將被設定在erro和doserro全局變量中。
由於_beginthread調用CreateThread,所以必須對_beginthread的返回值調用CloseHadle。
與ExitThread對應的c tuntime library函數名爲_endthreadex,該函數可以被任意線程在任意時間調用,它需要一個表示線程返回代碼的參數,事實上,當線程的startup函數返回時,_endthreadex會自動被runtime library調用。絕對不能在一個以_beginthreadex啓動的線程中調用ExitThread,這樣會導致 C runtime library沒有機會釋放爲該線程配置的資源。
應採用多線程版的C runtime library,並使用_beginthreadex和_endthreadex的情況如下:
①在C程序中使用malloc和free,或是在c++中使用new和delete
②調用stdio.h或io.h中聲明的任何函數,包括fopen,open,getchar,write,printf等,所有這些函數都用到共享的數據結構以及errno,可以使用wsprintf代替。
③使用浮點變量或浮點運算函數。
④調用任何一個使用了靜態緩衝區的runtime函數,如asctime,strtok或rand等函數。
爲避免使用stdio.h,須解決三個問題:
(1)通過使用wsprintf來解決字符串格式化問題,取代sprintf函數。
(2)利用函數GetStdHandle獲得取代C Runtime library中stdin,stdout,stderr的東西。
HANDLE GetStdHandle(
DWORD nStdHandle)
參數:
nStdHandle----設定傳回handle的型態,必須是以下三者之一:
STD_INPUT_HANDLE;STD_OUTPUT_HANDLE;
STD_ERROR_HANDLE
(3)利用Console API控制屏幕,Console APT提供對光標的控制,以及對字符屬性,窗口標題,鼠標等的控制。
爲適當清除C Runtime library中的結構,對於以_beginthread或_beginthreadex產生新線程的程序,應使用以下兩種技術結束程序:
(1)調用C runtime library中的exit函數
(2)從main返回系統
任何一種情況下,runtime library都會自動進行清理操作,最後調用ExitProcess,以上方法均不會等待線程的結束,任何正在運行的線程都會被自動終止。不到萬不得已不可調用abort函數,因爲調用該函數其間沒有任何退出程序可以調用,文件緩衝區也沒有清理。
關於_beginthread函數
unsigned long _beginthread(
void (_cdecl* start_address)(void*),
unsigned stack_size,
void* arglist)
參數:
Start_address----線程的起始函數,注意調用方式。
Stack_size----堆棧大小,以字節爲單位,與CreateThread相同,此值若爲0,表示使用默認大小。
Arglist----指向一塊數據,新線程將收到此指針,runtime library不會爲該數據另外拷貝一份。
返回值:如果失敗,返回-1,否則傳回一個unsigned long,代表新線程的handle,這個handle也許可用,也許不可用,調用端不見得可以安全地使用該handle,因爲利用該函數產生出來的線程所做的第一件事就是關閉自己的handle,沒有這個handle,也就沒有辦法等待這個線程的結束,改變其參數或取得其結束代碼。對應的線程結束函數是:
_endthread()不能指定結束代碼,對着一個以_beginthread產生出來的線程調用GetExitCodeThread是沒有意義的。
八.C++設計安全的多線程類
Class CLockableObject
{
Public:
CLockableObject(){}
virtual ~CLockableObject(){}
virtual void Lock() = 0;
virtual void Unlock() = 0;
};
Class CriticalSection:public CLockableObject
{
public:
CriticalSection();
{
InitializeCriticalSection(&m_CritSect);
}
virtual ~CriticalSection()
{
DeleteCriticalSection(&m_CritSect);
}
virtual void Lock()
{
EnterCriticalSection(&m_CritSect);
}
virtual void Unlock()
{
LeaveCriticalSection(&m_CritSect);
}
private:
CRITICAL_SECTION m_CritSect;
};
class Clock
{
public:
Clock(CLockableObject* pLockable)
{
m_pLockable = pLockable;
m_pLockable->Lock();
}
~Clock()
{
m_pLockable->Unlock();
}
pirvate:
CLockableObject* m_pLockable;
};
使用舉例:
Class StringV
{
Public:
StringV()
{
m_pData = NULL;
}
~StringV()
{
delete[] m_pData;
}
Void Set(char* str)
{
Clock localLock(&m_Lockable);
delete[] m_pData;
m_pData = NULL;
m_pData = new char[::strlen(str) + 1];
::strcpy(m_pData,str);
}
private:
CriticalSection m_Lockable;
char* m_pData;
}
九.MFC多線程
如果在MFC程序中產生一個線程,而該線程將調用MFC函數或使用MFC的任何數據,那麼你必須以AfxBeginThread或CWinThread::CreateThread來產生這些線程。
產生work線程的AfxBeingThread版本
CWinThread* AfxBeingThread(
AFX_THREADPROC pfnThreadProc,
LPVOID pParam,
int nPriority = THREAD_PRIORITY_NORMAL,
UINT nStackSize = 0,
DWORD dwCreateFlags = 0,
LPSECURITY_ATTRIBUTES lpSecurityAttrs = NULL)
參數:
pfnThreadProc----函數名稱,用來啓動線程
pParam----任意4字節數值,用來傳給新線程,它可以是整數或指針或爲0
nPriority----新線程的優先權,如果是缺省值,表示新線程的優先權將與目前優先權相同。
nStackSize----新線程的堆棧大小,0表示使用默認的堆棧大小。
dwCreateFlags----此值必須爲0或CREATE_SUSPENDED,如果爲0表示立刻開始新線程的生命。
lpSecurityAttrs----新線程的安全屬性,在Win95中忽略。
返回值:失敗返回NULL,成功返回指向新產生出來的CWinThread對象的指針。
CWinThread中有個成員變量m_bAutoDelete,這個參數可以阻止CWinThread對象被自動刪除,爲了能夠設定此變量而不產生一個死鎖,必須先以掛起狀態產生線程。
產生UI線程的AfxBeingThread版本
CWinThread* AfxBeginThread(
CRuntimeClass* pThreadClass,
int nPriority = THREAD_PRIORITY_NORMAL,
UINT nStackSize = 0,
DWORD dwCreateFlags = 0,
LPSECURITY_ATTRIBUTES lpSecurityAttrs = NULL)
參數:
pThreadClass---指向你所產生的一個類的runtime class,該類派生自CWinThread。
nPriority----新線程的優先權,如果是缺省值,表示新線程的優先權將與目前優先權相同。
nStackSize----新線程的堆棧大小,0表示使用默認的堆棧大小。
dwCreateFlags----此值必須爲0或CREATE_SUSPENDED,如果爲0表示立刻開始新線程的生命。
lpSecurityAttrs----新線程的安全屬性,在Win95中忽略。
返回值:失敗返回NULL,成功返回指向新產生出來的CWinThread對象的指針。
利用vc嚮導產生一個類CDemoThread,派生自CWinThread,使用AfxBeginThread產生新的UI線程。
CDemoThread* pThread = (CDemoThread*)AfxBeginThread(
RUNTIME_CLASS(CDemoThread));
一旦線程開始執行,MFC將調用你的派生類中的InitInstance成員函數,並進入消息循環中,你的CWinThread派生類的消息映射表將被作爲消息派送的指引。
MFC各對象和Win32 handle之間的映射關係記錄在線程局部存儲中,因此沒有辦法把一個MFC對象從某線程交到另一線程手上,也不能夠在線程之間傳遞MFC對象指針,這些對象包括CWnd,CDC,CPen,CBrush,CFont,CBitmap,CPalette等等。替代方案是:不要放置MFC對象,改放對象的handle,利用GetSafeHandle或GetSafeHwnd;當把handle傳遞給線程時,線程可以把該handle附着到一個新的MFC對象,使用FromHandle可以產生一個臨時對象,使用Attach可以附着到一個永久對象,但在退出之前應Detach。但是想在另一線程中調用比如UpdateAllViews這種函數,那麼只能發送一個用戶自定義消息,回到原線程中。另外出於效率的考慮,MFC中的CString類並沒有實現安全應用於線程的鎖定操作,要讓CString安全應用於多線程,應自己實現鎖定。
十.GDI與窗口管理
在Win16中,所有窗口共享同一個消息隊列,而在Win32中每一個線程有自己專屬的消息隊列。注意在Win32中,所有傳送給某一窗口的消息,將由產生該窗口的線程負責處理。
|
SendMessage |
PostMessage |
同一線程 |
直接調用窗口函數 |
把消息放到消息隊列中然後立刻返回。 |
不同的線程 |
切換到新線程中並調用窗口函數,在該窗口函數結束之前,SendMessage不會返回。 |
立刻返回,消息則被放到另一個線程的消息隊列中。 |
當一個線程(waiting thread)調研那個SendMessage向另一個線程(destination thread)發送消息,而SendMEssage尚未返回時,waiting thread依然可以處理來自SendMessage的消息,但不處理其他消息。爲解決這個問題,當destination thread調用以下任何函數時,waiting thread必須自動醒來。這些函數包括:DialogBox;DialogBoxIndirect; DirlogBoxIndirectParam;DialogBoxParam;GetMessage;MessageBox;
PeekMessage。或者爲了讓waiting thread能夠繼續工作,在destination thread中可以調用ReplyMessage函數。該函數允許destination thread窗口函數完成之前,waiting thread可繼續進行。另外可藉助函數SendMessageTimeOut,它允許你指定一個時間,時間終了後不管對方如何,一定返回。SendMessageCallback,該函數將指定的消息發送到一個或多個窗口。此函數爲指定的窗口調用窗口程序,並立即返回。當窗口程序處理完消息後,系統調用指定的回調函數,將消息處理的結果和一個應用程序定義的值傳給回調函數。如果發送到當前線程中,則直接調用窗口過程,發送到其它線程中則立即返回。
BOOL PostThreadMessage(
DWORD idThread,
UINT Msg,
WPARAM wParam,
LPARAM lParam)
參數:
idThread----線程ID,這個ID可由GetCurrentThreadId或CreateThread獲得。
Msg----消息識別代碼
wParam----消息的wParam。
lParam---消息的lParam。
返回值:如果消息被成功地post,返回TRUE,否則返回FALSE,可利用GetLastError獲得失敗原因。
如果在一個worker線程中調用GetMessage,該線程就會產生消息隊列,縱然它並沒有窗口,這時就可以調用PostThreadMessage給該worker線程發送消息。
PostThreadMessage把消息post給一個線程,而非一個窗口,如果收受端嘗試獲取目標窗口的handle,會得到NULL,使用PostThreadMessage在不同進程之間傳遞消息,必須使用WM_COPYDATA消息,這樣一來數據才能從一個地址空間中被映射到另一個地址空間。
十一、進程通信
(1)以消息進行通信:Windows爲進程間通信專門定義了一個消息,名爲WM_COPYDATA,專門用來在線程間搬移數據----不管兩個線程是否屬於同一個進程。WM_COPYDATA使用方式如下:SendMessage(hwndReceiver,WM_COPYDATA,(WPARAM)hwndSender,(LPARAM)&cds);其中cds爲一種特殊的數據結構:
typedef struct tagCOPYDATASTRUCT{
DWORD dwData;//用戶自定義值,通常用作一個行動代碼,指示//lpData中的內容的用途。
DWORD cbData;//lpData所指數據大小,以字節爲單位。
PVOID lpData//一塊數據,可以被傳送到接收端(目標窗口所屬線程)。
}
對於WM_COPYDATA消息,只能用SendMessage,而不能使用PostMessage或任何其他變種函數如PostThreadMessage等。絕對不可以把“指向某一擁有虛函數的對象”的指針當做lpData來傳遞,因爲vtbl指針將錯誤地指向別的進程中的函數。WM_COPYDATA是唯一一個可以在16位和32位程序之間搬移數據的方法。
(2)共享內存。
HANDLE CreateFileMapping(HANDLE hFile,
LPSECURITY_ATTRIBUTES lpFileMappinAttributes,DWORD flProtect, DWORD dwMaximumSizeHigh,DWORD dwMaximumSizeLow,LPCTSTR lpName)
參數:
hFile----這個參數正常而言應該是CreateFile傳回來的一個有關文件的handle,用以告訴系統將它映射到內存中。然而如果指定此參數爲0xffffffff,我們就可以使用頁面文件中的一塊空間,取代一般的文件。
lpFileMappinAttributes----安全屬性,在Win95中忽略。
flProtect----文件的保護屬性,可以是PAGE_READONLY或PAGE_READWRITE或PAGE_WRITECOPY,針對跨進程的共享內存,你應該指定此參數爲PAGE_READWRITE.
dwMaximumSizeHigh----映射的文件大小的高32位,如果使用頁面文件,此參數將總是0,因爲頁面文件沒有大到足夠容納4GB的共享內存空間。
dwMaximumSizeLow----映射區域的低32位,對於共享內存而言,此值應該是你要共享的內存大小。
lpName----共享內存區域的名稱,任何進程或線程都可以根據這個名稱,引用到這個文件映射對象,如果要產生共享內存,此參數不應該設爲NULL。
返回值:調用成功返回handle,否則返回NULL,調用GetLastError可獲得失敗的詳細原因。
爲了從共享內存中獲取指向可用內存的指針,調用MapViewOfFile函數。
LPVOID MapViewOfFile(
HANDLE hFileMappingObject,
DWORD dwDesiredAccess,
DWORD dwFileOffsetHigh,
DWORD dwFileOffsetLow,
DWORD dwNumberOfBytesToMap)
參數:
hFileMappingObject----文件映射核心對象的handle,這個調用CreateFileMapping或OpenFileMapping的返回值。
dwDesiredAccess----對共享內存而言,此值應該設爲FILE_MAP_ALL_ACCESS,其他目的則使用其他設定。
dwFileOffsetHigh----映射文件的高32位偏移值,如果使用頁面文件,該參數應總是爲0,因頁面文件不能容納4GB共享內存區域。
dwFileOffsetLow----映射文件的低32位偏移值,對於共享內存而言,該參數應總是爲0,以便映射整個共享區域。
dwNumberOfBytesToMap----真正要被映射的字節數量,如果指定爲0,表示要映射整個空間,所以對於共享內存而言,最簡單的做法是將此參數指定爲0.
返回值:成功則返回指向被映射出來的“視圖”的起始地址的指針,否則返回NULL,調用GetLastError找出原因。
HANDLE OpenFileMapping(
DWORD dwDesiredAccess,
BOOL bInheritHandle,
LPCTSTR lpName)
參數:
dwDesiredAccess----對於共享內存,此值應爲FILE_MAP_ALL_ACCESS,其他值適用於其他目的。
bInheritHandle----如果是TRUE,表示該handle可被子進程繼承。
lpName----共享內存的名稱,應該有另一個進程以相同的名稱調用CreateFileMapping。
返回值:成功返回一個handle,失敗返回NULL,可調用GetLastError獲取失敗信息。
一旦完成了對共享內存的操作,應調用UnmapViewOfFile,交出由MapViewOfFile所獲得的指針,然後再調用CloseHandle關閉file-mapping對象的handle。
BOOL UnmapViewOfFile(
LPVOID lpBaseAddress)
參數:
lpBaseAddress----指針,指向共享內存,這個值必須符合MapViewOfFile的返回值。
返回值:成功返回TRUE,失敗返回FALSE,可調用GetLastError獲取失敗信息。
注意一個特殊的指針修飾詞,_based,該修飾詞允許指針被定義爲從某一點開始起算的32位偏移值,而不是在內存中的絕對位置。例如以指針P爲基地址的指針。
int _based(P) *lpHead;
舉例如下:
extern SharedBlock* gpSharedBlock;//全局變量
struct SharedBlock
{
short m_nStringCount;
char __based( gpSharedBlock ) *m_pStrings[1];
};
void CShareMemDlg::OnWrite()
{
// Make sure the shared memory is available
::WaitForSingleObject(ghDataLock, INFINITE);
CEdit* pEdit = (CEdit*)GetDlgItem(IDC_EDIT);
ASSERT_VALID(pEdit);
int iLineCount = pEdit->GetLineCount();
gpSharedBlock->m_nStringCount = iLineCount;
char *pTextBuffer =
(char *)gpSharedBlock
+ sizeof(SharedBlock)
+ sizeof(char __based(gpSharedBlock) *) * (iLineCount-1);
char szLineBuffer[256];
while (iLineCount--)
{
pEdit->GetLine(iLineCount, szLineBuffer, sizeof(szLineBuffer)); szLineBuffer[pEdit->LineLength(pEdit->LineIndex(iLineCount))] = '\0';
strcpy(pTextBuffer, szLineBuffer);
gpSharedBlock->m_pStrings[iLineCount] =
(char _based(gpSharedBlock) *)pTextBuffer;
pTextBuffer += strlen(szLineBuffer) + 1;
}
::ReleaseMutex(ghDataLock);
}
共享內存使用注意事項:
(1)不要把C++ collection classes放到共享內存中
(2)不要把擁有虛函數的c++類對象放到共享內存中
(3)不要把CObject派生類的MFC對象放到共享內存中
(4)不要使用“point within the shared memory”的指針
(5)不要使用“point outside of shraed memory”的指針
(6)使用“based”指針是安全的,但要小心使用。
十二、動態鏈接庫
BOOL WINAPI DllMain(
HANDLE hinstDLL,DWORD fdwReason,LPVOID lpReserved)
參數:
hinstDLL----這個DLL的module handle。
fdwReason----DllMain被調用的原因,可能是以下之一:
DLL_PROCESS_ATTACH,DLL_PROCESS_DETACH
DLL_THREAD_ATTACH,DLL_THREAD_DETACH
lpReserved----提供更多信息以補充fdwReason,如果fdwReason是DLL_PROCESS_ATTACH,那麼lpReserved爲NULL表示DLL是被LoadLibrary載入,non—NULL表示DLL是被隱式載入,也就是在鏈接時期以import library和程序鏈接在一起。
返回值:如果fdwReason是DLL_PROCESS_ATTACH,那麼DllMain應該在成功時返回TRUE,失敗時返回FALSE,如果DLL是被隱式鏈接而DllMain返回的是FALSE,程序將沒有辦法執行下去,如果DLL是被顯式鏈接而DllMain返回FALSE,那麼LoadLibrary返回FALSE。如果fdwReason不是DLL_PROCESS_ATTACH,那麼返回值會被忽略。
任何時候當一個進程載入或卸載一個DLL時,DllMain會被調用,線程也是一樣,當一個進程開始時,它所用到得每一個DLL的DllMain都會被系統調用,並獲得DLL_PROCESS_ATTACH消息,如果是線程開始執行,進程所用到的每一個DLL的DllMain也都會被系統調用,並獲得DLL_THREAD_ATTACH消息。注意每個進程的第一個線程調用DllMain時,是以DLL_PROCESS_ATTACH調用,所有後續的線程是以DLL_THREAD_ATTACH調用。
函數DisableThreadLibrary可以抑制DllMain中DLL_THREAD_ATTACH和DLL_THREAD_DETACH消息。
BOOL DisableThreadLibrary(HMODULE hLibModule);
hLibModule----DLL的module handle。
返回值:成功返回TRUE,失敗返回FALSE,可調用GetLastError獲知詳細信息,如果你指定的DLL使用了線程局部存儲,這個函數調用一定會失敗。
如果DllMain收到DLL_PROCESS_ATTACH後,卻返回FALSE時,DllMain還是會收到DLL_PROCESS_DETACH。如果進程調用LoadLibrary時,有一個以上的線程正在運行,那麼DLL_THREAD_ATTACH不會針對每一個線程送出,而是隻有調用LoadLibrary的那個線程會發出。DllMain不接收任何因TerminateThread而結束的線程DLL_THREAD_DETACH通告消息。
一個使用MFC的DLL,擁有它自己的CWinThread對象,那可視爲CWinApp對象的一部分。當DLL接收到DLL_PROCESS_ATTACH時,MFC會調用InitInstance,當DLL接收到DLL_PROCESS_DETACH時,MFC會調用CWinThread::ExitThread,你可以提供自己的兩個函數,因爲它們都是虛函數,然而沒有任何虛函數在DLL_THREAD_ATTACH和DLL_THREAD_DETACH發生時調用。
TLS是一種機制,通過這種機制,線程可以持有一個指針,指向它自己的一份數據結構拷貝,C runtime library和MFC都是用TLS,C runtime library把errno和strtok指針放在TLS中,MFC通過TLS來追蹤每一個線程所使用的GDI對象和USER對象。這些對象只能夠用於產生它們的那些線程中,關於這點MFC是斤斤計較的,通過使用TLS,MFC就可以驗證對象是不是在線程間傳遞。
TLS的運行機制:系統中的每一個進程都有一個位數組,位數組的成員是一個標誌,每個標誌的值被設爲FREE或INUSE,指示了此標誌對應的數組索引是否在使用中,Windows保證至少有TLS_MINIMUM_AVAILABLE(在目前的操作系統中該值至少爲64)個標誌位可用。當一個線程被創建時,Windows會在進程地址空間中爲該線程分配一個長度爲TLS_MINIMUM_AVAILABLE的由4字節槽(slots)所組成的數組,數組成員的值都被初始化爲0,在內部,系統將此數組和該線程聯繫起來,保證只能在該線程中訪問此數組中的數據,每個線程都有它自己的數組,數組成員可以存儲任何數據。在調用TlsAlloc時,返回值是進程位數組的一個索引,這個位數組的唯一用途就是記憶哪一個下標在使用中,通過調用TlsAlloc,系統挨個檢查該位數組中成員的值,直到找到一個值爲FREE的成員,把找到的成員由FREE改爲INUSE後,函數返回該索引,若找不到返回TLS_OUT_OF_INDEXES,表示失敗。返回的這個索引被此進程中的每一個正在運行的和以後要被創建的線程保存起來,通過函數TlsSetValue和TlsGetValue訪問各自線程中的成員的值。
除了TLS之外,VC允許一個變量或結構被聲明爲具有線程局部性。_declspec(thread) DWORD 變量名,如果這樣聲明一個變量的話,該變量對每一個線程是獨一無二的。每一個以這種方式聲明對象的EXE或DLL,將在可執行文件中有一個特殊的節區,內含所有的線程局部變量,當EXE或DLL被載入時,操作系統會認識這個節區並適當處理,這個節區會被操作系統自動設定爲“對每一個線程具有局部性”。
Tls…函數和_declspec(thread)並不衝突,可以安全地混用。一個DLL如果使用了_declspec(thread),就沒有辦法被LoadLibrary載入。
若要使DLL能夠安全地在多線程環境中使用,需注意:
(1)不要使用全局變量,用來儲存TLS槽者例外。
(2)不要使用靜態變量。
(3)如有必要,儘量使用TLS。
(4)如有必要,儘量使用你的堆棧。