线程与内核对象的同步——Windows核心编程学习手札之九
線程與內核對象的同步
——Windows核心編程學習手札之九
用戶方式下的線程同步機制具有速度快的特點,但有其局限性,對于許多應用程序來說,并不合適。例如,互鎖函數家族只能在單值上運行,根本無法使線程進入等待狀態;關鍵代碼可以使線程進入等待狀態,但只能用關鍵代碼段對單個進程中的線程實施同步,使用關鍵代碼段也容易陷入死鎖,因為在等待進入關鍵代碼段時無法設定超時值。內核方式下的線程同步機制適應性要優于用戶方式下的機制,唯一不足的是內核對象機制的速度比較慢。內核對象機制下線程必須從用戶方式轉為內核方式,這個轉換需要很大代價:往返一次需要占用X86平臺上大約1000個CPU周期,當然還不包括執行內核方式代碼,即實現線程調用的函數的代碼所需時間。
對于線程同步來說,內核對象中的每種對象(包括進程、線程、作業)都可以說是處于已通知或未通知的狀態之中,這種狀態的切換是由Microsoft為每個對象建立的一套規則來決定。例如,進程內核對象總是未通知的狀態中創建的,當進程終止運行時,操作系統自動使該進程的內核對象處于已通知狀態,一旦進程內核對象得到通知,它將永遠保持這種狀態,它的狀態永遠不會改為未通知狀態。當進程正在運行時,進程內核對象處于未通知狀態,當進程終止運行時,就變為已通知狀態。線程的內核對象如是,因此可以將相同方法應用于程序。下面的內核對象可以處于已通知狀態或未通知狀態:1)進程;2)文件修改通知;3)線程;4)事件;5)作業;6)可等待定時器;7)文件;8)信標;9)控制臺輸入;10)互斥對象。線程可以使自己進入等待狀態,直到一個對象變為已通知狀態。注意,用于控制每個對象的已通知/未通知狀態的規則要根據對象類型而定。Windows提供專門實現線程同步的各種內核對象,如事件、等待計數器、信標和互斥對象。
等待函數可使線程資源進入等待狀態,直到一個特定的內核對象變為已通知狀態為止,這些等待函數中最常用的是WaitForSingleObject:
DWORD WaitForSingleObject(
?????????????????????????????????? HANDLE hObject,
?????????????????????????????????? DWORD dwMillisecondes);
當線程調用該函數時,第一個參數hObject標識一個能夠支持被通知/未通知的內核對象,第二個參數dwMilliseconds運行線程設置等待的時間長度。
函數WaitForMultipleObjects允許調用線程同時查看若干個內核對象的已通知狀態:
DWORD WaitForMultipleObjects(
?????????????????????????????????? DWORD dwCount,
?????????????????????????????????? CONST HANDLE *phObjects,
?????????????????????????????????? BOOL fWailAll,
?????????????????????????????????? DWORD dwMilliseconds);
該函數的參數dwCount用于指明想要讓函數查看的內核對象的數量,這個值必須在1和MAXIMUN_WAIT_OBJECTS(在Windows頭文件中定義為64)之間;參數phObjects參數是指向內核對象句柄的數組的指針。參數fWaitAll告訴函數是在指定內核對象中任何一個變為已通知狀態還是在所有指定的內核對象都變為已通知狀態下線程可運行,如fWaitAll傳遞了TRUE,那么在所有對象變為已通知前,該函數將不運行調用線程運行;dwMilliseconds與WaitForSingleObject作用一樣,用于設置等待時長。
事件內核對象是最基本的對象,包含一個使用計數(與所有內核對象一樣)、一個用于指明該事件是個自動重置的事件還是一個人工重置的事件的布爾值、用于指明該事件處于已通知狀態還是未通知狀態的布爾值。事件能夠通知一個操作已經完成,有兩種不同類型的事件對象,一種是人工重置的事件,另一種是自動重置的事件。當人工重置的事件得到通知時,等待該事件的所有線程均變為可調度的線程;當一個自動重置的事件得到通知時,等待該事件的線程中只有一個線程變為可調度的線程。當一個線程執行初始化操作,然后通知另一個線程執行剩余的操作時,事件使用得最多。事件初始化為未通知狀態,在該線程完成它的初始化操作后,它將事件設置為已通知狀態,這時,等待該事件的另一個線程發現該事件已經得到通知,此線程序就變成可調度線程。
創建事件內核對象:
HANDLE CreateEvent(
?????????????????????????????????? PSECURITY_ATTRIBUTES psa,
?????????????????????????????????? BOOL fManualReset,
?????????????????????????????????? BOOL fInitialState,
?????????????????????????????????? PCTSTR pszName);
其中,fManualReset參數是布爾值,告訴系統創建人工重置的事件(TRUE)或是自動重置事件(FALSE);fInitialState參數用于指明該事件是要初始化為已通知狀態(TRUE)還是未通知狀態(FALSE);當系統創建事件對象后,createEvent就將與進程相關的句柄返回給事件對象,其他進程中的線程可以獲得對該對象的訪問權,方法是使用在pszName參數中傳遞的相同值,使用繼承性,使用DuplicateHandle函數等來調用CreateEvent,或者調用OpenEvent,在pszName參數中設定一個與調用createEvent時設定的名字相匹配的名字:
HANDLE OpenEvent(
??????????????????????????? DWORD fdwAccess,
??????????????????????????? BOOL fInherit,
??????????????????????????? PCTSTR pszName);
與其他內核對象一樣,當不再需要事件內核對象時,應調用CloseHandle函數。
一旦事件已經創建,就可以直接控制它的狀態,當調用SetEvent時,可以將事件改為已通知狀態:BOOL SetEvent(HANDLE hEvent);當調用ResetEvent(HANDLE hEvent)函數時,就可以將該事件改為未通知狀態。函數BOOL PulseEvent(HANDLE hEvent)使得事件變為已通知狀態,然后立即變為未通知狀態,如調用SetEvent后又立即調用ResetEvent函數。
下面的代碼例子說明了自動重置事件的用法,具體是實現了由主線程向子線程發起請求時設置事件未通知狀態,子線程響應主線程序請求:
#include <process.h>
//this event is signaled when the client has a request for server
HANDLE g_hEventRequestSubmitted;
//this event is signaled when the server has a result for the client
HANDLE g_hEventResultReturned;
//the buffer shared between the client an server threads
TCHAR g_szSharedRequestAndResultBuffer[1024];
//server thread to terminate cleanly
TCHAR g_szServerShutdown[]=TEXT("Server Shutdown");
?
//this is the code executed by the server thread
UINT WINAPI ServerThread(PVOID pvParam)
{
?????? //assume that the server thread is to run forever
?????? BOOL fShutdown=FALSE;
?????? while(!fShutdown)
?????? {
????????????? //wait for the client to submit a request
????????????? WaitForSingleObject(g_hEventRequestSubmitted,INFINITE);
????????????? //check to see if the client wants the server to terminate
????????????? fShutdown=(lstrcmpi(g_szSharedRequestAndResultBuffer,g_szServerShutdown)==0);
????????????? if(!fShutdown)
????????????? {
???????????????????? //process the client's requet(reverse the string)
???????????????????? _tcsrev(g_szSharedRequestAndResultBuffer);
????????????? }
?
????????????? //let the client process the request's result
????????????? SetEvent(g_hEventResultReturned);
?????? }
?????? return 0;
}
?
void CEventDemoDlg::OnOK()
{
?????? // TODO: Add extra validation here
?????? //create & initialize the two nonsignaled ,auto-reset events
?????? g_hEventRequestSubmitted=CreateEvent(NULL,FALSE,FALSE,NULL);
?????? g_hEventResultReturned=CreateEvent(NULL,FALSE,FALSE,NULL);
?
?????? //spawn the server thread
?????? UINT dwThreadID;
?????? HANDLE hThreadServer=(HANDLE)_beginthreadex(NULL,0,ServerThread,NULL,0,&dwThreadID);
?
?????? CString strReq;
?????? GetDlgItemText(IDC_EditReq,strReq);
?????? strcpy(g_szSharedRequestAndResultBuffer,strReq);
?
?????? //let the server thread know that a request is ready in the buffer
?????? SetEvent(g_hEventRequestSubmitted);
?
?????? //wait for the server to process the request and give us the result
?????? WaitForSingleObject(g_hEventResultReturned,INFINITE);
?????? //let the user know the result
?????? strReq.Format(_T("%s"), g_szSharedRequestAndResultBuffer);
?????? SetDlgItemText(IDC_EditRes,strReq);
?
?
?????? //end
?????? lstrcpy(g_szSharedRequestAndResultBuffer,g_szServerShutdown);
?????? SetEvent(g_hEventRequestSubmitted);
?????? //wait for the server thread to acknowledge the shutdown and wait for the server thread to fully terminate
?????? HANDLE h[2];
?????? h[0]=g_hEventResultReturned;
?????? h[1]=hThreadServer;
?????? WaitForMultipleObjects(2,h,TRUE,INFINITE);
?????? //properly clean up everything
?????? CloseHandle(hThreadServer);
?????? CloseHandle(g_hEventRequestSubmitted);
?????? CloseHandle(g_hEventResultReturned);
?????? //close the application
//???? CDialog::OnOK();
?????? AfxMessageBox("close the application");
}
void CEventDemoDlg::OnOK() 是VC6.0中Dialog工程的一個按鈕函數,在這里具體實現了事件的狀態變化來同步線程的運行。
等待定時器內核對象是在某個時間或按規定的間隔時間發出自己的信號來通知,用在某個時間執行某個操作,其函數是:
HANDLE CreateWaitableTimer(
??????????????????????????? PSECURITY_ATTRIBUTES psa,
??????????????????????????? BOOL fManualReset,
??????????????????????????? PCTSTR pszName);
進程可以獲得與自己相關的現有等待定時器的句柄,通過函數:
HANDLE OpenWaitableTimer(
??????????????????????????? DWORD dwDesiredAccess,
??????????????????????????? BOOL bInheritHandle,
??????????????????????????? PCTSTR pszName);
與事件內核對象一樣,fManualReset參數用于指明人工重置的定時器或自動重置的定時器。當發出人工重置的定時器信號通知時,等待該定時器的所有線程均變為可調度的線程,當發出自動重置的定時器信號通知時,只有一個等待的線程變為可調度線程。
等待定時器總是在未通知狀態中創建,必須調用SetWaitableTimer函數來告訴定時器使其變為已通知狀態:
BOOL SetWaitableTimer(
??????????????????????????? HANDLE hTimer,
??????????????????????????? Const LARGE_INTEGER *pDueTime,
??????????????????????????? LONG lPeriod,
??????????????????????????? PTIMERAPCROUTINE pfnCompletionRoutine,
??????????????????????????? PVOID pvArgToCompletionRoutine,
??????????????????????????? BOOL fResume);
其中,參數hTimer用于指明所設置的定時器;PDueTime和lPeriod兩個參數是一同使用的,PDueTime指明定時器何時應第一次報時,而lPeriod參數則指明此后定時器應間隔多長時間報時一次,下面代碼用于將定時器的第一次報時設置在2009年1月8日的下午一點鐘,然后每隔6小時報時一次:
//Declare our local variables.
HANDLE hTimer;
SYSTEMTIME st;
FILETIME ftLocal,ftUTC;
LARGE_INTEGER liUTC;
//create an auto-reset timer.
hTimer=CreateWaitableTimer(NULL,FALSE,NULL);
//first signaling is at January 8, 2009 at 1:00P.M. (local time).
st.wYear=2009;
st.wMonth=1;
st.wDayofWeek=0;
st.wDay=8;
st.wHour=13;
st.wMinute=0;
st.wSeconde=0;
st.wMilliseconds=0;
SystemTimeToFileTime(&st,&ftLocal);
//convert local time to UTC time.
LocalFileTimeToFileTime(&ftLocal,&ftUTC);
//convert FILETIME to LARGE_INTEGER because of different alignment.
liUTC.LowPart=ftUTC.dwLowDateTime;
liUIC.HighPart=ftUTC.dwHighDateTime;
//set the timer.
SetWaitableTimer(hTimer,&liUTC,6*60*60*1000,NULL,NULL,FALSE);
……
首先對SYSTEMTIME結構進行初始化,該結果用于指明定時器何時第一次報時(發出信號通知)。
信標內核對象用于對資源進行計數,與所有內核對象一樣,包含一個使用數量,但也包含另外兩個帶符號的32位值,一個是最大資源數量,一個是當前資源數量,最大資源數量用于標識信標能夠控制的資源的最大數量,而當前資源數量則用于標識當前可以使用的資源的數量。信標的使用規則是:1)如果當前資源的數量大于0,則發出信標信號;2)如果當前資源數量是0,則不發出信標信號;3)系統決不允許當前資源的數量為負值;4)當前資源數量決不能大于最大資源數量。當使用信標時,不要將信標對象的使用數量和它的當前資源數量混為一談。創建信標內核對象的函數是:
HANDLE CreateSemaphore(
?????????????????????????????????? PSECURITY_ATTRIBUTE psa,
?????????????????????????????????? LONG lInitialCount,
?????????????????????????????????? LONG lMaximumCount,
?????????????????????????????????? PCTSTR pszName);
通過函數OpenSemaphore函數,其他進程可以獲得現有信標有關的句柄:
HANDLE OpenSemaphore(
?????????????????????????????????? DWORD fdwAccess,
?????????????????????????????????? BOOL bInheritHandle,
?????????????????????????????????? PCTSTR pszName);
lMaximumCount參數用于告訴系統,應用程序處理的最大資源數量是多少,該參數是帶符號的32位值,因此最多可以擁有2147483647個資源。lInitialCount參數用于指明開始時(當前)這些資源有多少可供使用。信標能以原子操作方式執行測試和設置操作,也就是說,當向信標申請一個資源時,操作系統就要檢查是否有這個資源可供使用,同時將可用資源的數量遞減,而不讓另一個線程加以干擾,只有當資源數量遞減后,系統才允許另一個線程申請對資源的訪問權。通過調用ReleaseSemaphore函數,線程能夠對信標的當前資源數量進行遞增:
BOOL ReleaseSemaphore(
?????????????????????????????????? HANDLE hsem,
?????????????????????????????????? LONG lReleaseCount,
?????????????????????????????????? PLONG plPreviousCount);
該函數將lReleaseCount中的值添加給信標的當前資源數量。
互斥對象(mutex)內核對象能夠確保線程擁有對單個資源的互斥訪問權,包含一個使用數量,一個線程ID和一個遞歸計數器,互斥對象的行為特性和關鍵代碼段相同,但是互斥對象屬于內核對象,而關鍵代碼段則屬于用戶方式對象,這意味著互斥對象的運行速度比關鍵代碼段要慢,但也意味著不同進程的多個線程能夠訪問單個互斥對象,并且線程在等待訪問資源時可以設定一個超時值。ID用于標識系統中的那個線程當前擁有互斥對象,遞歸計數器用于指明該線程擁有互斥對象的次數?;コ鈱ο笫浅S脙群藢ο笾?#xff0c;通常是用于保護由多個線程訪問的內存塊,互斥對象保證訪問內存塊的任何線程擁有對該內存塊的獨占訪問權,這樣可保證數據的完整性?;コ鈱ο笫褂靡巹t如下:
1)? 如果線程ID是0(這是無效的ID值),互斥對象不被任何線程所擁有,并且發出該互斥對象的通知信號;
2)? 如果ID是非0數字,那么一個線程就擁有互斥對象,并且不發出該互斥對象的通知信號;
3)? 與所有其他內核對象不同,互斥對象在操作系統中擁有特殊的代碼,允許它們違反正常的規則;
若要使用互斥對象,需要調用CreateMutex來創建互斥內核對象:
HANDLE CreateMutex(
??????????????????????????? PSECURITY_ATTRIBUTES psa,
??????????????????????????? BOOL fInitialOwner,
??????????????????????????? PCTSTR pszName);
通過調用OpenMutex,另一個進程可以獲得現有互斥對象相關的句柄:
HANDLE OpenMutex(
??????????????????????????? DWORD fdwAccess,
??????????????????????????? BOOL bInheritHandle,
??????????????????????????? PCTSTR pszName);
其中,參數fInitialOwner用于控制互斥對象的初始狀態,如果傳遞FALSE(通常情況下傳遞的值),那互斥對象的ID和遞歸計數器被設置為0,這意味著該互斥對象沒有被任何線程所擁有,因此要發出它的通知信號。如果fInitialOwner參數傳遞TRUE,那么該對象的線程ID被設置為調用線程的ID,遞歸計數器被設置為1,由于ID是個非0數字,因此該互斥對象開始時不發出通知信號。
對于互斥對象來說,正常的內核對象的已通知和未通知規則存在一個特殊的異常情況,比如說:一個線程試圖等待一個未通知的互斥對象,在這種情況下,該線程通常被置于等待狀態,然而系統要查看試圖獲取互斥對象的線程ID是否與互斥對象中記錄的線程ID相同,如果相同,即使互斥對象處于未通知狀態,系統也允許線程保持可調度狀態,這種“異?!毙袨樘匦圆贿m用于系統中的其他內核對象。每當線程成功地等待互斥對象時,該對象的遞歸計數器就遞增,若要使遞歸計數器的值大于1,唯一的方法是線程多次等待相同的互斥對象,以便利用這個異常規則。一旦線程成功等待到一個互斥對象,該線程就知道它已經擁有對受保護資源的獨占訪問權,試圖訪問該資源的任何其他線程(通過等待相同的互斥對象)均被置于等待狀態中。當目前擁有對資源訪問權的線程不再需要它的訪問權時,需要調用ReleaseMutex函數來釋放該互斥對象:
BOOL ReleaseMutex(HANDLE hMutex);
該函數將對象遞歸技數器遞減1,如果線程多次成功等待一個互斥對象,在互斥對象的遞歸計數器變成0之前,該線程必須以同樣次數調用ReleaseMutex函數,當遞歸計數器到達0時,該線程ID也被置為0,同時該對象變為已通知狀態。當一個線程調用ReleaseMutex函數時,函數要查看調用線程的ID是否與互斥對象中的線程ID相匹配,如相同,遞歸計數器就遞減,如不匹配,那函數將不進行任何操作,而是將FALSE(表示失敗)返回給調用者,此時調用GetLastError,將返回ERROR_NOT_OWNER(試圖釋放不是調用者擁有的互斥對象)。如在釋放互斥對象之前,擁有互斥對象的線程終止運行(使用ExitThread、TerminateThread、ExitProcess或TerminateProcess函數),系統將該互斥對象視為已經被放棄——擁有互斥的線程決不會釋放它,因為該線程已經終止運行。
總結下線程同步對象(內核對象)與線程同步之間的相互關系:
1)對象:進程,何時處于未通知狀態:當進程仍然活動時,何時處于已通知狀態:當進程終止運行時(ExitProcess,TerminateProcess),成功等待的副作用:無;
2)對象:線程,何時處于未通知狀態:當線程仍然活動時,何時處于已通知狀態:當線程終止運行時(ExitThread,TerminateThread),成功等待的副作用:無;
3)對象:作業,何時處于未通知狀態:當作業的時間尚未結束時,何時處于已通知狀態:當作業的時間結束時,成功等待的副作用:無;
4)對象:文件,何時處于未通知狀態:當I/O請求正在處理時,何時處于已通知狀態:當I/O請求處理完畢時,成功等待的副作用:無;
5)對象:控制臺輸入,何時處于未通知狀態:不存在任何輸入,何時處于已通知狀態:當存在輸入時,成功等待的副作用:無;
6)對象:文件修改通知,何時處于未通知狀態:沒有任何文件被修改,何時處于已通知狀態:當文件系統發現修改時,成功等待的副作用:重置通知;
7)對象:自動重置事件,何時處于未通知狀態:ResetEvent,Pulse-Event或等待成功,何時處于已通知狀態:當調用SetEvent/PulseEvent時,成功等待的副作用:重置事件;
8)對象:人工重置事件,何時處于未通知狀態:ResetEvent,Pulse-Event,何時處于已通知狀態:當調用SetEvent/PulseEvent時,成功等待的副作用:無
9)對象:自動重置等待定時器,何時處于未通知狀態:CancelWaitableTimer或等待成功,何時處于已通知狀態:當時間到時(SetWaitableTimer),成功等待的副作用:重置定時器;
10)對象:人工重置等待定時器,何時處于未通知狀態:CancelWaitableTimer,何時處于已通知狀態:當時間到時(SetWaitableTimer),成功等待的副作用:無;
11)對象:信標,何時處于未通知狀態:等待成功,何時處于已通知狀態:當數量>0時(ReleaseSemaphore),成功等待的副作用:數量遞減1;
12)對象:互斥對象,何時處于未通知狀態:等待成功,何時處于已通知狀態:當未被線程擁有時(ReleaseMutex),成功等待的副作用:將所有權賦予線程;
13)對象:關鍵代碼段(用戶方式),何時處于未通知狀態:等待成功((Try)EnterCriticalSection),何時處于已通知狀態:當未被線程擁有時(LeaveCriticalSection),成功等待的副作用:將所有權賦予線程。
互鎖(用戶方式)函數決不會導致線程變為非調度狀態,它們會改變一個值并立即返回。
線程同步函數WaitForSingleObject和WaitForMultipleObjects是使用最多的函數,不過,Windows還提供了其他函數。異步設備I/O使得線程能夠啟動一個讀操作或寫操作,但是不必等待讀操作或寫操作完成。例如,如果線程需要將一個大文件裝入內存,那該線程可以告訴系統將文件裝入內存,在系統加載該文件時,線程可以執行其他任務,如創建窗口、對內部數據結構進行初始化等等,當初始化操作完成時,線程可以終止自己的運行,等待系統通知文件已經讀取。設備對象是可以同步的內核對象,這意味著可以調用WaitForSingleObject函數,傳遞文件、套接字和通信端口的句柄。當系統執行異步I/O時,設備對象處于未通知狀態,一旦操作完成,系統就將對象的狀態改為已通知狀態,這樣,線程就知道操作已經完成,此時線程可繼續運行。線程可以調用WaitForInputIdle來終止自己的運行:
DWORD WaitForInputIdle(
??????????????????????????? HANDLE hProcess,
??????????????????????????? DWORD dwMilliseconds);
該函數一直處于等待狀態,知道hProcess標識的進程在創建應用程序的第一個窗口的線程中已經沒有尚未處理的輸入為止。
線程可以調用MsgWaitForMultipleObjects或MsgWaitForMultipleObjectsEx函數,讓線程等待它自己的消息:
DWORD MsgWaitForMultipleObjects(
????????????????????????????????????????? DWORD dwCount,
????????????????????????????????????????? PHANDLE phObjects,
????????????????????????????????????????? BOOL fWaitAll,
????????????????????????????????????????? DWORD dwMilliseconds,
????????????????????????????????????????? DWORD dwWakeMask);
DWORD MsgWaitForMultipleObjects(
????????????????????????????????????????? DWORD dwCount,
????????????????????????????????????????? PHANDLE phObjects,
????????????????????????????????????????? BOOL fWaitAll,
????????????????????????????????????????? DWORD dwMilliseconds,
????????????????????????????????????????? DWORD dwWakeMask,
DWORD dwFlags);
這些函數與WaitForMultipleObjects函數十分相似,差別在于它們允許線程在內核對象變成已通知狀態或窗口消息需要調度到調用線程創建的窗口中時被調度。
線程同步函數還有WaitForDebugEvent函數:當調試程序啟動運行時,它將自己附加給一個被調試程序;SingleObjectAndWait函數用于在單個原子方式的操作中發出關于內核對象的通知并等待另一個內核對象。
?????????????????????????????????? 如非
????????????????????????????????????? 2009-1-15
《新程序員》:云原生和全面數字化實踐50位技術專家共同創作,文字、視頻、音頻交互閱讀總結
以上是生活随笔為你收集整理的线程与内核对象的同步——Windows核心编程学习手札之九的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 线程的调度、优先级和亲缘性——Windo
- 下一篇: 用户方式中线程的同步——Windows核