WINDOWS 同步(Interlocked,InterlockedExchangeAdd,Slim讀/寫鎖,WaitForSingleObject,CreateWaitableTimer等等)
NOTE0
在以下兩種基本情況下,線程之間需要相互通信:
- 需要讓多個線程同時訪問一個共享資源,同時不能破壞資源的完整性;
- 一個線程需要通知其它線程某項任務已經完成
1.原子訪問:Interlocked系列函數
http://hi.baidu.com/microsoftxiao/blog/item/a6411546296bc90c6a63e561.html該文章不錯。
所謂原子訪問,指的是一個線程在訪問某個資源的同時能夠保證沒有其他線程會在同一時刻訪問同一資源。
我們需要有一種方法能夠保證對一個值的遞增操作時原子操作——也就是說,不會被打斷。Interlocked系列函數提供了我們需要的解決方案。
PLONG volatile plAddend,
LONG lIncrement);
LONGLONG InterlockedExchangeAdd64(
PLONGLONG volatile pllAddend,
LONGLONG llIncrement);
只要調用這個函數,傳一個長整形變量的地址和另一個增量值,函數就會保證遞增操作是以原子方式進行的。
Interlocked 函數又是如何工作的呢?取決於代碼運行的CPU平臺。如果是x86系列CPU,那麽Interlocked函數會在總線上維持一個硬件信號,這個信號會阻止其它CPU訪問同一個內存地址。無論編譯器如何生成代碼,無論機器上裝配了多少個CPU,這些函數都能夠保證對值的修改時以原子方式進行的。
當然,也可以用InterlockedExchangeAdd來做減法——只要在第二個參數中傳入一個負值就行了。
下面是其它三個Interlocked函數:
LONG InterlockedExchange(
PLONG volatile plTarget,
LONG lValue);
LONGLONG InterlockedExchange64(
LONGLONG lValue);
PVOID InterlockedExchangePointer(
PVOID* volatile ppvTarget,
PVOID pvValue);
實現自旋鎖的時候,InterlockedExchange及其有用:
// Global variable indicating whether a shared resource is in use or not
BOOL g_fResourceInUse = FALSE; ...
void Func1() {
// Wait to access the resource.
while (InterlockedExchange (&g_fResourceInUse, TRUE) == TRUE)
Sleep(0);
// Access the resource.
...
// We no longer need to access the resource. InterlockedExchange(&g_fResourceInUse, FALSE);
}
在單CPU的機器上應避免使用旋轉鎖 。
PVOID InterlockedCompareExchange(
PLONG plDestination,
LONG lExchange,
LONG lComparand);
PVOID InterlockedCompareExchangePointer(
PVOID* ppvDestination,
PVOID pvExchange,
PVOID pvComparand);
該函數對當前值( plDestination 參數指向的值)與 lComparand 參數中傳遞的值進行比較。如果兩個值相同,那麽* plDestination 改為 lExchange 參數的值。如果* plDestination 中的值與 lExchange 的值不匹配, * plDestination 保持不變。該函數返回* plDestination 中的原始值。記住,所有這些操作都是作為一個原子執行單位來進行的。
2.高級線程同步
如果我們只需要以原子方式修改一個值,那麽Interlocked系列函數非常好用,我們當然應該優先使用它們。為了能夠以“原子”方式訪問復雜的數據結構,我們必須超越Interlocked系列函數。
我們既不應該使用旋轉鎖,也不應該進行輪循,因為浪費CPU時間是件很糟糕的事情。而應該調用函數把線程切換到等待狀態,直到線程想要訪問的資源可供使用為止。
volatile關鍵字:
volatile限定符告訴編譯器這個變量可能被應用程序之外的其它東西修改,比如操作系統、硬件或者一個並發執行的線程。確切地說,volatile限定符告訴編譯器不要對這個變量進行任何形式的優化,而是始終從變量在內存中的位置讀取變量的值。給一個結構加volatile限定符等於給結構中所有的成員都加volatile限定符,這樣可以確保任何一個成員始終都是從內存中讀取的。
3.關鍵段
關鍵段 (critical section)是一小段代碼,它在執行之前需要獨占對一些共享資源的訪問權。這種方式可以讓多行代碼以“原子方式”來對資源進行操控。即,代碼知道除了當前線程之外,沒有任何線程會同時訪問該資源。當然,系統仍然可以暫停當前線程去調度其它線程。但是,在當前線程離開關鍵段之前,系統是不會去調度任何想要訪問同一資源的其它線程的。
一般情況下,我們會將CRITICAL_SECTION結構作為全局變量來分配,這樣進程中的所有線程就能夠非常方便地通過變量名來訪問這些結構。在使用 CRIICAL_SECTION的時候,只有兩個必要條件:第一條件是所有想要訪問資源的線程必須知道用來保護資源的CRITICAL_SECTION結構的地址(我們可以通過自己喜歡的任何方式來把這個地址傳給各個線程)。第二個條件是在任何線程試圖訪問被保護的資源之前,必須對 CRITICAL_SECTION結構的內部成員進行初始化。
下面這個函數用來對結構進行初始化:
VOID InitializeCriticalSection(PCRITICAL_SECTION pcs);
當知道線程不再需要訪問共享資源的時候,我們應該調用下面的函數來清理CRITICAL_SECTION結構:
VOID DeleteCriticalSection(PCRITICAL_SECTION pcs);
然後我們在以下兩個函數之間訪問共享資源:
VOID EnterCriticalSection(PCRITICAL_SECTION pcs);
。。。共享資源的訪問。。。
VOID LeaveCriticalSection(PCRITICAL_SECTION pcs);
EnterCriticalSection會執行下面的測試:
•如果沒有線程正在訪問該資源, EnterCriticalSection 便更新成員變量,以表示調用線程已被賦予訪問權並立即返回,使該線程能夠繼續運行(訪問該資源)。
•如果成員變量表明調用線程已經被賦予對資源的訪問權,那麽 EnterCriticalSection 便更新這些變量,以指明調用線程多少次被賦予訪問權並立即返回,使該線程能夠繼續運行。這種情況很少出現,並且只有當線程在一行中兩次調用 EnterCriticalSection 而不影響對 LeaveCriticalSection 的調用時,才會出現這種情況。
•如果成員變量指明,一個線程(除了調用線程之外)已被賦予對資源的訪問權,那麽 EnterCriticalSection 將調用線程置於等待狀態。這種情況是極好的,因為等待的線程不會浪費任何CPU時間。系統能夠記住該線程想要訪問該資源並且自動更新 CRITICAL_SECTION 的成員變量,一旦目前訪問該資源的線程調用 LeaveCriticalSection 函數,該線程就處於可調度狀態。
我們可以用下面的函數的函數來代替EnterCriticalSection:
BOOL TryEnterCriticalSection(PCRITICAL_SECTION pcs);
TryEnterCriticalSection從來不會讓調用線程進入等待狀態。 它會通過返回值來表示調用線程是否獲準訪問資源。如果資源正在被其它線程訪問,那麽返回值為FALSE,其它為TRUE。如果返回TRUE,那麽CRITICAL_SECTION的成員已經更新過了,以表示該線程正在訪問資源。因此,每個返回TRUE的 TryEnterCriticalSection調用必須有一個對應的 LeaveCriticalSection 。
當不能用Interlocked函數解決同步問題的時候,我們應該試一試關鍵段。關鍵段的最大好處在於它們非常容易使用,而且它們在內部也使用了Interlocked函數,因此執行速度非常快。關鍵段的最大缺點在於它們無法用來在多個進程之間對線程進行同步。
當線程試圖進入一個關鍵段,但這個關鍵段正被另一個線程占用的時候,函數會立即把調用線程切換到等待狀態。這意味著線程必須從用戶模式切換到內核模式(大約1000個CPU周期),這個切換的開銷非常大。為了提高關鍵段的性能,Microsoft把旋轉鎖合並到了關鍵段中。因此,當調用 EnterCriticalSection的時候,它會用一個旋轉鎖不斷地循環,嘗試在一段時間內獲得對資源的訪問權。只有嘗試失敗的時候,線程才會切換到內核模式並進入等待狀態。
為了在使用關鍵段的時候同時使用旋轉鎖,我們必須調用下面的函數來初始化關鍵段:
http://blog.csdn.net/yuntongsf/archive/2009/07/31/4396451.aspx
BOOL InitializeCriticalSectionAndSpinCount(
PCRITICAL_SECTION pcs,
DWORD dwSpinCount);
dwSpinCount是我們希望旋轉鎖循環的次數。 這個值可以是0~0x00FFFFFF之間的任何一個值。在單處理器的機器上調用這個函數,那麽函數會忽略 dwSpinCount參數,因此次數總是0。因為如果一個線程正在循環,那麽占用資源的線程將沒有機會放棄對資源的訪問權。
我們可以調用一下函數來改變關鍵段的旋轉次數:
DWORD SetCriticalSectionSpinCount(
PCRITICAL_SECTION pcs,
DWORD dwSpinCount);
用來保護進程堆的關鍵段鎖使用的旋轉次數大約是4000,這可以作為我們的一個參考值。
4.Slim讀/寫鎖
SRWLock的目的和關鍵段相同:對一個資源進行保護,不讓其它線程訪問它。但是,與關鍵段不同的是,SRWLock允許我們區分哪些想要讀取資源的值的線程(讀取者線程)和想要更新資源的值的線程(寫入者線程)。讓所有的讀取者線程在同一時刻訪問共享資源應該是可行的,這是因為僅僅讀取資源的值並不存在破壞數據的風險。只有當寫入者線程想要對資源進行更新的時候才需要進行同步。在這種情況下,寫入者線程想要對資源進行更新的時候才需要進行同步。在這種情況下,寫入者線程應該獨占對資源的訪問權:任何其它線程,無論是讀取者線程還是寫入者線程,都不允許訪問資源。這就是SRWLock提供的全部功能。
首先,我們需要分配一個SRWLOCK結構並用InitializeSRWLock函數對它進行初始化:
VOID InitializeSRWLock(PSRWLOCK SRWLock);
一旦SRWLock初始化完成之後,寫入者線程就可以調用AcquireSRWLockExclusive,將SRWLOCK對象的地址作為參數傳入,以嘗試獲得對被保護資源的獨占訪問權。
VOID AcquireSRWLockExclusive(PSRWLOCK SRWLock);
完成對資源的更新之後,應該調用ReleaseSRWLockExclusice,並將SRWLOCK對象的地址作為參數傳入,這樣就解除了對資源的鎖定。
VOID ReleaseSRWLockExclusive(PSRWLOCK SRWLock);
對讀取者線程來說,同樣有兩個步驟,單調用的是下面兩個新的函數:
VOID AcquireSRWLockShared(PSRWLOCK SRWLock);
VOID ReleaseSRWLockShared(PSRWLOCK SRWLock);
不存在用來刪除或銷毀SRWLOCK的函數,系統會自動執行清理工作。
與關鍵段相比,SRWLock缺乏下面兩個特性:
•不存在TryEnter(Shared/Exclusive)SRWLock 之類的函數:如果鎖已經被占用,那麽調用AcquireSRWLock(Shared/Exclusive) 會阻塞調用線程。
•不能遞歸地調用SRWLOCK。也就是說,一個線程不能為了多次寫入資源而多次鎖定資源,然後再多次調用ReleaseSRWLock* 來釋放對資源的鎖定。
總結一下,如果希望在應用程序中得到最佳性能,那麽首先應該嘗試不要共享數據,然後依次使用volatile讀取,volatile寫入,Interlocked API,SRWLock以及關鍵段。當且僅當所有這些都不能滿足要求的時候,再使用內核對象。因為每次等待和釋放內核對象都需要在用戶模式和內核模式之間切換,這種切換的CPU開銷非常大。
5.一些有用的竅門和技巧
•以原子方式操作一組對象時使用一個鎖;
•同時訪問多個邏輯資源時以完全相同的順序來獲得資源的鎖;
•不要長時間占用鎖;
條件變量
Condition variables —— 條件變量,是Windows Vista中新增加的一種處理線程同步問題的機制。它可以與“關鍵代碼段(critical section)”或“讀寫鎖(SRWLock)”相互配合使用,來實現線程的同步,特別是實現類似“生產者-消費者”問題的時候,十分有效。
如果當前沒有“產品”可供“消費者線程”(讀者線程)使用,那麽該“消費者線程”要釋放掉對應的讀寫鎖或者關鍵代碼段,然後等待直到有一個新的“產品”被“生產者線程”(寫者線程)制造出來之後,方可繼續運行。
如果一個用來存放“產品”的數據結構滿了(比如數組),那麽對應“生產者線程”需要釋放有關的鎖和關鍵代碼段,同時要等待“消費線程者”消費完這些“產品”。
條件變量機制就是為了簡化上述“生產者-消費者”問題而設計的一種線程同步機制。當一個線程需要以原子的方式釋放加在資源上的鎖,並且需要被阻塞運行直到一個條件被滿足,你可以呼叫如下的函數:
BOOL SleepConditionVariableCS(PCONDITION_VARIABLE pConditionVariable,
PCRITICAL_SECTION pCriticalSection,
DWORD dwMilliseconds); BOOL SleepConditionVariableSRW(
PCONDITION_VARIABLE pConditionVariable,
PSRWLOCK pSRWLock,
DWORD dwMilliseconds,
ULONG Flags);
正如函數名所預示的那樣,第一個函數針對關鍵代碼段,第二個函數針對讀寫鎖。
第1個參數pContidionVariable參數指向一個初始化的條件變量,該條件變量指明了調用者(一個線程)的條件變量,參數類型是CONDITION_VARIABLE的指針。
第2個參數指明了一個“關鍵代碼段”或“讀寫鎖”,它們是用來保護共享資源的。
第3個參數dwMilliseconds指明了你的線程想要等待多長時間,你可以傳遞INFINITE,指明需要無限期等待下去。
第二個函數的第4個參數Flags指明當條件變量滿足的時候,你需要對應的鎖做如何的要求(即返回的時候設置鎖,該鎖應該是什麽類型的):在“生產者線程”(寫者線程)中你應該傳遞0給該參數指明該鎖是一個“排他鎖”,該鎖被線程獨占;在“消費者線程”(讀者線程)中你應該傳遞CONDITION_VARIABLE_LOCKMODE_SHARED給該參數指明該鎖是一個“共享鎖”,該鎖能以共享的方式為“消費者線程”服務。
在該參數調用的時候,第二個參數所指定的關鍵代碼段或讀寫鎖會被釋放,使得對應的線程可以訪問共享資源,從而去“生產”或“消費”;在該函數返回的時候,這個鎖又會被設置。如果是SRWLock,該函數返回的時候根據Flags參數設置讀寫鎖類型:排他鎖或共享鎖。關鍵代碼段則會被自動設置,因為關鍵代碼段總是“排他”的。
如果等待超時,該函數返回FALSE,否則返回TRUE。
一個線程當被SleepConditionVariableCS或SleepConditionVariableSRW阻塞之後,可以被另一個線程通過呼叫WakeConditionVariable或WakeAllConditionVariable函數喚醒。
VOID WakeConditionVariable(PCONDITION_VARIABLE ConditionVariable); //條件變量指針 VOID WakeAllConditionVariable(
PCONDITION_VARIABLE ConditionVariable); //條件變量指針
當你呼叫WakeConditionVariable函數的時候,傳遞一個條件變量的指針給它,此時,在一個等待在同樣條件變量的上的線程的SleepConditionVariable函數內部,條件變量收到信號,通知線程,該函數會返回,同時把對應的鎖設定為所需要的類型。
當你呼叫WakeAllConditionVarialbe函數的時候,一個過多個等待在相同條件變量上的線程會被被喚醒。喚醒多個線程是可以的,但是你需要在調用SleepConditionVariable*函數的時候設定參數Flags:給“生產者線程”傳遞0;給“消費者線程”傳遞CONDITION_VARIABLE_LOCKMODE_SHARED。所以,有些時候,“消費者線程”全部會被喚醒。或者這樣喚醒:生產者、消費者、生產者、消費者……如此循環。
本書還舉了一個例子,這裏就不多說了,我個人總結了下,運用條件變量應該遵循如下模式(自己是這麽認為的,如果有誤還大家請指出)
CONDITION_VARIABLE g_cvProduce; //生產條件變量
CONDITION_VARIABLE g_cvConsume; //消費條件變量
SRWLOCK g_srwLock; //讀寫鎖
DWORD WINAPI Consumer(PVOID pvParam) //消費者線程函數
{
AcquireSRWLockShard(&g_srwLock); //請求共享鎖(讀鎖)
SleepConditionVariableSRW(g_cvConsume, &g_srwLock, INFINITE, CONDITION_VARIABLE_LOCKMODE_SHARED); //等待條件變量,會被生產者線程喚醒
//消費
ReleaseSRWLockShared(&g_srwLock); //釋放共享鎖
WakeConditionVariable(&g_cvProduce); //喚醒一個生產者線程
}
DWORD WINAPI Producer(PVOID pvParam) //生產者線程函數
{
AcquireSRWLockExclusive(&g_srwLock); //要求一個排他鎖(寫鎖)
//等待條件變量受信,會被消費者線程喚醒
SleepConditionVariableSRW(g_cvProduce, &g_srwLock, INFINITE, 0);
//生產
用戶模式的線程同步機制效率高,如果需要考慮線程同步問題,應該首先考慮用戶模式的線程同步方法。
但是,用戶模式的線程同步有限制,對於多個進程之間的線程同步,用戶模式的線程同步方法無能為力。這時,只能考慮使用內核模式。
Windows提供了許多內核對象來實現線程的同步。對於線程同步而言,這些內核對象有兩個非常重要的狀態:“已通知”狀態,“未通知”狀態(也有翻譯為:受信狀態,未受信狀態)。Windows提供了幾種內核對象可以處於已通知狀態和未通知狀態:進程、線程、作業、文件、控制臺輸入/輸出/錯誤流、事件、等待定時器、信號量、互斥對象。
你可以通知一個內核對象,使之處於“已通知狀態”,然後讓其他等待在該內核對象上的線程繼續執行。你可以使用Windows提供的API函數,等待函數來等待某一個或某些內核對象變為已通知狀態。
你可以使用WaitForSingleObject函數來等待一個內核對象變為已通知狀態:
DWORD WaitForSingleObject(HANDLE hObject, //指明一個內核對象的句柄
DWORD dwMilliseconds); //等待時間
該函數需要傳遞一個內核對象句柄,該句柄標識一個內核對象,如果該內核對象處於未通知狀態,則該函數導致線程進入阻塞狀態;如果該內核對象處於已通知狀態,則該函數立即返回WAIT_OBJECT_0。第二個參數指明了需要等待的時間(毫秒),可以傳遞INFINITE指明要無限期等待下去。如果等待超時,該函數返回WAIT_TIMEOUT。如果該函數失敗,返回WAIT_FAILED。可以通過下面的代碼來判斷:
DWORD dw = WaitForSingleObject(hProcess, 5000); //等待一個進程結束
switch (dw)
{
case WAIT_OBJECT_0:
// hProcess所代表的進程在5秒內結束
break;
case WAIT_TIMEOUT:
// 等待時間超過5秒
break;
case WAIT_FAILED:
// 函數調用失敗,比如傳遞了一個無效的句柄
break;
}
還可以使用WaitForMulitpleObjects函數來等待多個內核對象變為已通知狀態:
DWORD WaitForMultipleObjects(DWORD dwCount, //等待的內核對象個數
CONST HANDLE* phObjects, //一個存放被等待的內核對象句柄的數組
BOOL bWaitAll, //是否等到所有內核對象為已通知狀態後才返回
DWORD dwMilliseconds); //等待時間
該函數的第一個參數指明等待的內核對象的個數,可以是0到MAXIMUM_WAIT_OBJECTS(64)中的一個值。phObjects參數是一個存放等待的內核對象句柄的數組。bWaitAll參數如果為TRUE,則只有當等待的所有內核對象為已通知狀態時函數才返回,如果為FALSE,則只要一個內核對象為已通知狀態,則該函數返回。第四個參數和WaitForSingleObject中的dwMilliseconds參數類似。
該函數失敗,返回WAIT_FAILED;如果超時,返回WAIT_TIMEOUT;如果bWaitAll參數為TRUE,函數成功則返回WAIT_OBJECT_0,如果bWaitAll為FALSE,函數成功則返回值指明是哪個內核對象收到通知。
可以如下使用該函數:
HANDLE h[3]; //句柄數組
//三個進程句柄
h[0] = hProcess1;
h[1] = hProcess2;
h[2] = hProcess3;
DWORD dw = WaitForMultipleObjects(3, h, FALSE, 5000); //等待3個進程結束
switch (dw)
{
case WAIT_FAILED:
// 函數呼叫失敗
break;
case WAIT_TIMEOUT:
// 超時
break;
case WAIT_OBJECT_0 + 0:
// h[0](hProcess1)所代表的進程結束
break;
case WAIT_OBJECT_0 + 1:
// h[1](hProcess2)所代表的進程結束
break;
case WAIT_OBJECT_0 + 2:
// h[2](hProcess3)所代表的進程結束
break;
}
你也可以同時通知一個內核對象,同時等待另一個內核對象,這兩個操作以原子的方式進行:
DWORD SignalObjectAndWait(HANDLE hObjectToSignal, //通知的內核對象
HANDLE hObjectToWaitOn, //等待的內核對象
DWORD dwMilliseconds, //等待的時間
BOOL bAlertable); //與IO完成端口有關的參數,暫不討論
該函數在內部使得hObjectToSignal參數所指明的內核對象變成已通知狀態,同時等待hObjectToWaitOn參數所代表的內核對象。dwMilliseconds參數的用法與WaitForSingleObject函數類似。
該函數返回如下:WAIT_OBJECT_0,WAIT_TIMEOUT,WAIT_FAILED,WAIT_IO_COMPLETION。
等你需要通知一個互斥內核對象並等待一個事件內核對象的時候,可以這麽寫:
ReleaseMutex(hMutex);WaitForSingleObject(hEvent, INFINITE);
可是,這樣的代碼不是以原子的方式來操縱這兩個內核對象。因此,可以更改如下:
SignalObjectAndWait(hMutex, hEvent, INFINITE, FALSE);
本書首先介紹了一個重要的概念“成功的副作用”,這裏筆者作一下簡述。
當調用WaitForSingleObject和WaitForMultipleObject函數成功之後,該函數在返回成功的時候,系統可能會自動更改所等待的內核對象的狀態,即將其從“已通知狀態”切換為“未通知狀態”。
當一個內核對象的狀態被更改,稱之為“成功等待的副作用”。比如,一個“自動重置”的事件內核對象,當調用等待函數成功返回的時候,該事件內核對象會由已通知狀態轉變為未通知狀態。
比如此時有一個自動重置的事件內核對象hEvent,它處於未通知狀態。線程T1、T2、T3內部調用“WaitForSingleObject(hEvent, INFINITE);”,這樣當該事件內核對象變為“已通知”狀態的話,T1線程“可能”被喚醒,但是其他的線程T2和T3呢?由於在T1線程內部WaitForSingleObject函數返回成功,又將hEvent事件內核對象設置為“未通知”狀態,那麽T2和T3就不可能被喚醒。
也就是說,“成功等待的副作用”會導致多個等待在同一個內核對象上的線程只能被喚醒一個。
好,下面我們來討論“事件內核對象”。
在所有內核對象中,事件內核對象是最基本的一個內核對象。在事件內核對象內部,有以下幾個比較重要的數據:
1、有一個“引用計數”:指明被打開的次數;
2、一個“布爾值”:指明該事件內核對象是自動重置的還是人工重置的;
3、另一個“布爾值”:指明該事件內核對象是“已通知狀態”還是“未通知狀態”。
事件內核對象可以通知一個事件已經完成。有兩種不同的類型:自動重置和人工重置。當人工重置的事件內核對象得到通知的時候,所有等待在事件內核對象上的線程都變成可調度線程。當一個自動重置的事件內核對象得到通知的時候,等待在該事件內核對象上的線程只有一個能變成可調度狀態。
要使用事件內核對象,首先調用CreateEvent函數來創建一個事件內核對象:
HANDLE CreateEvent(PSECURITY_ATTRIBUTES psa,
BOOL bManualReset,
BOOL bInitialState,
PCTSTR pszName);
參數psa是一個SECURITY_ATTRIBUTES(安全屬性)結構的指針,一般設置為默認安全,傳遞NULL。
bManualReset參數指定了該內核對象是人工重置(傳遞TRUE)的還是自動重置(傳遞FALSE)的。
bInitialState參數指定了該內核對象起始狀態是已通知(傳遞TRUE)還是未通知狀態(FALSE)。
pszName參數為要創建的事件內核對象起一個名字,如果傳遞NULL,則創建一個“匿名”的事件內核對象。如果不傳遞NULL,且系統中已經存在該名字的事件內核對象,則不創建新的事件內核對象而是打開這個已經存在的,返回它的句柄。
該函數如果成功,返回事件內核對象的句柄,這樣就可以操縱它了。如果失敗,返回NULL。
Windows Vista提供了另一個函數來創建事件內核對象:
HANDLE CreateEventEx(PSECURITY_ATTRIBUTES psa,
PCTSTR pszName,
DWORD dwFlags,
DWORD dwDesiredAccess);
該函數的psa和pszName參數的意義和函數CreateEvent相同。
參數dwFlags可以有以下數據的“位或組合”:
WinBase.h中定義的位組合數據 |
描述 |
---|---|
CREATE_EVENT_INITIAL_SET(0x00000002) |
如果設置了該數據,則表明事件內核對象的起始狀態為已通知狀態;否則起始狀態為未通知狀態。 |
CREATE_EVENT_MANUAL_RESET(0x00000001) |
如果設置了該數據,則表明事件內核對象是人工重置的;否則為自動重置的。 |
參數dwDesiredAccess可以讓你對該事件內核對象的訪問加一些限制,本書沒有細說,查MSDN就可以了吧。
可以打開一個“命名”的事件內核對象:
HANDLE OpenEvent(DWORD dwDesiredAccess,
BOOL bInherit,
PCTSTR pszName);
第一個參數指明的訪問的限制,第二個參數表示該事件內核對象的句柄能夠被子進程繼承,第三個參數指明了該事件內核對象的名字。該函數成功返回事件內核對象的句柄,失敗返回NULL。
當不需要使用這些句柄時,需要調用CloseHandle函數來遞減內核對象的引用計數,使得該內核對象可以被及時清除。
當一個事件內核對象被創建之後,你可以直接控制它的狀態。你可以通知它,使得它從未通知狀態轉變為已通知狀態:
BOOL SetEvent(HANDLE hEvent);
也可以重新設置它,使它從已通知狀態變為未通知狀態:
BOOL ResetEvent(HANDLE hEvent);
一個自動重置的事件內核對象,如果等待成功,由於“成功等待的副作用”機制會將該事件內核對象由已通知狀態變為未通知狀態,這個時候就沒有必要調用ResetEvent函數了。
如果是一個人工重置的事件內核對象,等待成功之後,並不會被設置為未通知狀態,而是要程序員調用ResetRvent函數來使之轉變為未通知狀態。
還有要註意的就是,一個“自動重置”的事件內核對象收到通知,轉變為已通知狀態的時候,最多只能喚醒“一個”等待在它上的線程。一個“人工重置”的事件內核對象收到通知,轉變為已通知狀態的時候,能夠喚醒“所有”等待在它上的線程。
等待定時器(waitable timer)是在某個時間或按規定的時間間隔通知自己的內核對象。可以把它理解為一個定時發送信號的東西。
要創建一個等待定時器內核對象,可以調用函數CreateWaitableTimer。可以為該函數賦予不同的參數來指定一個定時器內核對象的屬性。
HANDLE CreateWaitableTimer(PSECURITY_ATTRIBUTES psa,
BOOL bManualReset,
PCTSTR pszName);
該函數第一個參數是安全屬性結構指針。第三個參數是要創建的定時器內核對象名稱。第二個參數指明了該定時器內核對象是人工重置(TRUE)的還是自動重置(FALSE)的。該函數成功,返回句柄,失敗則返回NULL。
當一個人工重置的定時器內核對象收到通知時,所有等待在該內核對象上的線程都可以被喚醒,進入就緒狀態。一個自動重置的定時器內核對象收到通知時,只有一個等待在該內核對象上的線程可以被調度。
當然,也可以打開一個特定名字的定時器內核對象,呼叫OpenWaitableTimer函數:
HANDLE OpenWaitableTimer(DWORD dwDesiredAccess,
BOOL bInheritHandle,
PCTSTR pszName);
等待定時器內核對象創建的時候的狀態總是“未通知狀態”。你可以呼叫SetWaitableTimer函數來設定等待定時器內核對象何時獲得通知。
BOOL SetWaitableTimer(HANDLE hTimer, //等待定時器句柄
const LARGE_INTEGER *pDueTime, //第一次通知的時刻(負數表示相對值)
LONG lPeriod, //以後通知的時間間隔(毫秒)
PTIMERAPCROUTINE pfnCompletionRoutine, //APC異步函數地址
PVOID pvArgToCompletionRoutine, //APC異步函數參數
BOOL bResume); //是否讓計算機擺脫暫停狀態
該函數的第1個參數hTimer是一個等待定時器內核對象的句柄。
第2個參數pDutTime和第3個參數lPeriod要聯合使用,pDutTime是一個LAGRE_INTEGER結構指針,指明了第一次通知的時間,時間格式是UTC(標準時間),是一個絕對值,如果要設置一個相對值,即讓等待定時器在調用SetWaitableTimer函數之後多少時間發出第一次通知,只要傳遞一個負數給該參數即可,但是該數值必須是100ns的倍數,即單位是100ns,下面會舉例說明。
第3個參數指明了以後通知的時間間隔,以毫秒為單位,該參數為0時,表示只有第一次的通知,以後沒有通知。
第4和第5這兩個參數與APC(異步過程調用)有關,這裏不討論。
最後一個參數bResume支持計算機暫停和恢復,一般傳遞FALSE。當它為TRUE的時候,當定時器通知的時候,如果此時計算機處於暫停狀態,它會使計算機脫離暫停狀態,並喚醒等待在該等待定時器上的線程。如果它為FALSE,如果此時計算機處於暫停狀態,那麽當該定時器通知的時候,等待在該等待定時器上的線程會被喚醒,但是要等待計算機恢復運行之後才能得到CPU時間。
比如,下面代碼使用等待定時器讓它在2008年8月8日晚上8:00開始通知。然後每隔1天通知。
HANDLE hTimer; //等待定時器句柄SYSTEMTIME st; //SYSTEMTIME結構,用來設置第1次通知的時間
FILETIME ftLocal, ftUTC; //FILETIME結構,用來接受STSTEMTIME結構的轉換
LARGE_INTEGER liUTC; //LARGE_INTEGER結構,作為SetWaitableTimer的參數
// 創建一個匿名的默認安全性的人工重置的等待定時器內核對象,並保存句柄
hTimer = CreateWaitableTimer(NULL, FALSE, NULL);
//設置第一次通知時間
st.wYear = 2008; // 年
st.wMonth = 8; // 月
st.wDayOfWeek = 0; // 一周中的某個星期
st.wDay = 8; // 日
st.wHour = 20; // 小時(下午8點)
st.wMinute = 8; // 分
st.wSecond = 0; // 秒
st.wMilliseconds = 0; // 毫秒
//將SYSTIME結構轉換為FILETIME結構
SystemTimeToFileTime(&st, &ftLocal);
//將本地時間轉換為標準時間(UTC),SetWaitableTimer函數接受一個標準時間
LocalFileTimeToFileTime(&ftLocal, &ftUTC);
// 設置LARGE_INTEGER結構,因為該結構數據要作為SetWaitableTimer的參數
liUTC.LowPart = ftUTC.dwLowDateTime;
liUTC.HighPart = ftUTC.dwHighDateTime;
// 設置等待定時器內核對象(一天的毫秒數為24*60*60*1000)
SetWaitableTimer(hTimer, &liUTC, 24 * 60 * 60 * 1000,
NULL, NULL, FALSE);
下面的代碼創建了一個等待定時器,當調用SetWaitableTimer函數之後2秒會第一次通知,然後每隔1秒通知一次:
HALDLE hTimer;LARGE_INTEGER li;
hTimer = CreateWaitableTime(NULL, FALSE, NULL);
const int nTimerUnitsPerSecond = 100000000 / 100; //每1s中有多少個100ns
li.QuadPart = -(2 * nTimerUnitsPerSecond ); //負數,表示相對值2秒
SetWaitableTimer(hTimer, &li, 1000, NULL, NULL, FALSE);
當通過SetWaitTimer函數設置了一個等待定時器的屬性之後,你可以通過CancelWaitableTimer函數來取消這些設置:
BOOL CancelWaitableTimer(HANDLE hTimer);
當你不再需要等待定時器的時候,通過調用CloseHanble函數關閉之。
等待定時器與APC(異步過程調用)項排隊:
Windows允許在等待定時器的通知的時候,那些調用SetWaitTimer函數的線程的異步過程調用(APC)進行排隊。
要使用這個特性,需要在線程調用SetWaitTimer函數的時候,設置第4個參數pfnCompletionRoutine和第5的參數pvArgToCompletionRoutine。這個異步過程需要如下形式:
VOID APIENTRY TimerAPCRoutine(PVOID pvArgToCompletionRoutine,DWORD dwTimerLowValue, DWORD dwTimerHighValue)
{
// 特定的任務
}
該函數名TimerAPCRoutine可以任意。該函數可以在等待定時器收到通知的時候,由調用SetWaitableTimer函數的線程來調用,但是該線程必須處於“待命等待”狀態。也就是說你的線程因為調用以下函數的而處於等待狀態中:SleepEx,WaitForSingleObjectEx,WaitForMultipleObjectEx,MsgForMultipleObjectEx,SingleObjectAndWait。如果該線程沒有因為調用這些函數而進入等待狀態,那麽系統不會給定時器APC排隊。
下面講一下詳細的APC調用的過程:當你的等待定時器通知的時候,如果你的線程處於“待命等待”狀態,那麽系統就調用上面具有TimerAPCRoutine異步函數的格式的函數,該異步函數的第一個參數就是你傳遞給SetWaitableTimer函數的第5個參數pvArgToCompletionRoutine的值。其他兩個參數用於指明定時器什麽時候發出通知。
下面的代碼指明了使用等待定時器的正確方法:
void SomeFunc(){
// 創建一個等待定時器(人工重置)
HANDLE hTimer = CreateWaitableTimer(NULL, TRUE, NULL);
// 當調用SetWaitableTimer時候立刻通知等待定時器
LARGE_INTEGER li = { 0 };
SetWaitableTimer(hTimer, &li, 5000, TimerAPCRoutine, NULL, FALSE);
// 線程進入“待命等待”狀態,並無限期等待
SleepEx(INFINITE, TRUE);
CloseHandle(hTimer); //關閉句柄
}
當所有的APC項都完成,即所有的異步函數都結束之後,等待的函數才會返回(比如SleepEx函數)。所以,必須確保等待定時器再次變為已通知之前,異步函數就完成了,這樣,等待定時器的APC排隊速度不會比它的處理速度慢。
註意,當使用APC機制的時候,線程不能應該等待“等待定時器的句柄”,也不應該以待命等待的方式等待“等待定時的句柄”,下面的方法是錯誤的:
HANDLE hTimer = CreateWaitableTimer(NULL, FALSE, NULL);
SetWaitableTimer(hTimer, &li, 2000, TimerAPCRoutine, NULL, FALSE);
WaitForSingleObjectEx(hTimer, INFINITE, TRUE);
這段代碼讓線程2次等待一個等待定時器,一個是等待該等待定時器的句柄,還有一個是“待命等待”。當定時器變為已通知狀態的時候,該等待就成功了,然後線程被喚醒,導致線程擺脫了“待命等待”狀態,APC函數不會被調用。
由於等待定時器的管理和重新設定是比較麻煩的,所以一般開發者很少使用這個機制,而是使用CreateThreadpoolTimer來創建線程池的定時器來處理問題。
等待定時器的APC機制也往往被I/O完成端口所替代。
最後,把“等待定時器”和“用戶界面定時器”做一下比較。
用戶界面定時器是通過SetTimer函數設置的,定時器一般發送WM_TIMER消息給調用SetTimer函數的線程和窗口,因此只能有一個線程收到通知。而“人工重置”的等待定時器可以讓多個線程同時收到通知。
運用等待定時器,可以讓你的線程到了規定的時間就收到通知。而用戶界面定時器,發送的WM_TIMER消息屬於最低優先級的消息,當線程隊列中沒有其他消息的時候才會檢索該消息,因此可能會有一點延遲。
另外,WM_TIMER消息的定時精度比較低,沒有等待定時器那麽高。
http://blog.csdn.net/caichao1234/article/details/8927054
WINDOWS 同步(Interlocked,InterlockedExchangeAdd,Slim讀/寫鎖,WaitForSingleObject,CreateWaitableTimer等等)