c#多线程同步之EventWaitHandle使用
原文鏈接:http://www.cnblogs.com/swneng/p/10081210.html
c#多線程同步之EventWaitHandle使用
文章原始出處?http://xxinside.blogbus.com/logs/47523285.html
預(yù)備知識(shí):C#線程同步(1)- 臨界區(qū)&Lock,C#線程同步(2)- 臨界區(qū)&Monitor,C#線程同步(3)- 互斥量 Mutex
WaitHandle一家
在前一篇我們已經(jīng)提到過(guò)Mutex和本篇的主角們直接或間接繼承自WaitHandle:
Mutex類,這個(gè)我們?cè)谏弦黄呀?jīng)講過(guò)。
EventWaitHandle 類及其派生類AutoResetEvent 和 ManualResetEvent,這是本篇的主角。
Semaphore 類,即信號(hào)量,我們下一篇再講。
WaitHandle提供了若干用于同步的方法。上一篇關(guān)于Mutex的blog中已經(jīng)講到一個(gè)WaitOne(),這是一個(gè)實(shí)例方法。除此之外,WaitHandle另有3個(gè)用于同步的靜態(tài)方法:
SignalAndWait(WaitHandle, WaitHandle):以原子操作的形式,向第一個(gè)WaitHandle發(fā)出信號(hào)并等待第二個(gè)。即喚醒阻塞在第一個(gè)WaitHandle上的線程/進(jìn)程,然后自己等待第二個(gè)WaitHandle,且這兩個(gè)動(dòng)作是原子性的。跟WaitOne()一樣,這個(gè)方法另有兩個(gè)重載方法,分別用Int32或者TimeSpan來(lái)定義等待超時(shí)時(shí)間,以及是否從上下文的同步域中退出。
WaitAll(WaitHandle[]):這是用于等待WaitHandle數(shù)組里的所有成員。如果一項(xiàng)工作,需要等待前面所有人完成才能繼續(xù),那么這個(gè)方法就是一個(gè)很好的選擇。仍然有兩個(gè)用于控制等待超時(shí)的重載方法,請(qǐng)自行參閱。
WaitAny(WaitHandle[]):與WaitAll()不同,WaitAny只要等到數(shù)組中一個(gè)成員收到信號(hào)就會(huì)返回。如果一項(xiàng)工作,你只要等最快做完的那個(gè)完成就可以開(kāi)始,那么WaitAny()就是你所需要的。它同樣有兩個(gè)用于控制等待超時(shí)的重載。
線程相關(guān)性(Thread Affinity )
EventWaitHandle和Mutex兩者雖然是派生自同一父類,但有著完全不同的線程相關(guān)性:
Mutex與Monitor一樣,是“線程相關(guān)(Thread Affinity)”的。我們之前已經(jīng)提到過(guò),只有通過(guò)Monitor.Enter()/TryEnter()獲得對(duì)象鎖的線程才能調(diào)用Pulse()/Wait()/Exit();同樣的,只有獲得Mutex擁有權(quán)的線程才能執(zhí)行ReleaseMutex()方法,否則就會(huì)引發(fā)異常。這就是所謂的線程相關(guān)性。
相反,EventWaitHandle以及它的派生類AutoResetEvent和ManualResetEvent都是線程無(wú)關(guān)的。任何線程都可以發(fā)信號(hào)給EventWaitHandle,以喚醒阻塞在上面的線程。
下一篇要提到的Semaphore也是線程無(wú)關(guān)的。
Mutex與Event
我們?cè)贛utex一篇中沒(méi)有具體提到Mutex是否能發(fā)送信號(hào),只是簡(jiǎn)單說(shuō)Mutex不太適合有相互消息通知的同步,它僅有的一些同步方法是來(lái)自其父類的靜態(tài)方法。那么現(xiàn)在我們可以仔細(xì)來(lái)看看Mutex到底能不能用于關(guān)于Monitor那篇提到的生產(chǎn)者、消費(fèi)者和糖罐的場(chǎng)景。
回過(guò)頭來(lái)仔細(xì)查看Mutex的所有方法,除了一個(gè)我們已經(jīng)提到的WaitHandle上的靜態(tài)方法SingnalAndWait(toSingnal, toWaitOn),我們找不到任何“屬于Mutex自己”的、用于發(fā)送信號(hào)的方法。退而求其次吧,我們就來(lái)看看這個(gè)靜態(tài)方法是否可以讓Mutex具有通知的能力。
如果toSignal是一個(gè)Mutex,那么收到“信號(hào)”就等效于ReleaseMutex()。而由于Mutex的線程相關(guān)性,只有擁有當(dāng)前Mutex的線程才能夠發(fā)送這個(gè)信號(hào)(ReleaseMutex),否則會(huì)引發(fā)異常。也就是說(shuō)如果要用這個(gè)方法來(lái)通知其它線程同步,Mutex只能自己發(fā)給自己。與之相反,如果第二個(gè)參數(shù)toWaitOn也是個(gè)Mutex,那么這個(gè)Mutex不能是自己。因?yàn)榍捌呀?jīng)講過(guò),Mutex的擁有者可以多次WaitOne()而不阻塞,這里也是一樣。所以如果Mutex一定要使用這個(gè)方法,準(zhǔn)確的說(shuō)是只是成為這個(gè)方法的參數(shù),那只能是WaitHandle.SignalAndWait(它自己,另一個(gè)Mutex)。
試想,如果有人試圖只使用Mutex來(lái)進(jìn)行同步通知。假設(shè)生產(chǎn)者線程通過(guò)Mutex上的WaitOne()獲得了mutexA的擁有權(quán),并且在生產(chǎn)完畢后調(diào)用了SingnalAndWait(mutexA,mutexB),通知由于當(dāng)前mutexA而阻塞的消費(fèi)者線程,并且將自己阻塞在mutexB上。那么被喚醒的消費(fèi)者線程獲得MutexA的擁有權(quán)吃掉糖后,也只能調(diào)用SingnalAndWait(mutexA,mutexB)釋放它獲得的mutexA且阻塞于MutexB。問(wèn)題來(lái)了,此時(shí)的生產(chǎn)者是阻塞在mutexB上……也許,我們可以設(shè)計(jì)一段“精巧”的代碼,讓生產(chǎn)者和消費(fèi)者一會(huì)兒阻塞在mutexA,一會(huì)兒阻塞在mutexB上……我不想花費(fèi)這個(gè)力氣去想了,你可以試試看:)。不管有沒(méi)有這樣的可能,Mutex很明顯就不適用于通知的場(chǎng)景。
EventWaitHandle的獨(dú)門秘笈
正因?yàn)镸utex沒(méi)有很好地繼承父輩的衣缽,EventWaitHandle以及它的兒子/女兒們便來(lái)到了這個(gè)世界上。
EventWaitHandle、AutoResetEvent、ManualResetEvent名字里都有一個(gè)“Event”,不過(guò)這跟.net的本身的事件機(jī)制完全沒(méi)有關(guān)系,它不涉及任何委托或事件處理程序。相對(duì)于我們之前碰到的Monitor和Mutex需要線程去爭(zhēng)奪“鎖”而言,我們可以把它們理解為一些需要線程等待的“事件”。線程通過(guò)等待這些事件的“發(fā)生”,把自己阻塞起來(lái)。一旦“事件”完成,被阻塞的線程在收到信號(hào)后就可以繼續(xù)工作。
為了配合WaitHandle上的3個(gè)靜態(tài)方法SingnalAndWait()/WailAny()/WaitAll(),EventWaitHandle提供了自己獨(dú)有的,使“Event”完成和重新開(kāi)始的方法:
bool:Set():英文版MSDN:Sets the state of the event to signaled, allowing one or more waiting threads to proceed;中文版MSDN:將事件狀態(tài)設(shè)置為終止?fàn)顟B(tài),允許一個(gè)或多個(gè)等待線程繼續(xù)。初看“signaled”和“終止”似乎并不對(duì)應(yīng),細(xì)想起來(lái)這兩者的說(shuō)法其實(shí)也不矛盾。事件如果在進(jìn)行中,當(dāng)然就沒(méi)有“終止”,那么其它線程就需要等待;一旦事件完成,那么事件就“終止”了,于是我們發(fā)送信號(hào)喚醒等待的線程,所以“信號(hào)已發(fā)送”狀態(tài)也是合理的。兩個(gè)小細(xì)節(jié):
無(wú)論中文還是英文版,都提到這個(gè)方法都是可以讓“一個(gè)”或“多個(gè)”等待線程“繼續(xù)/Proceed”(注意不是“喚醒”)。所以這個(gè)方法在“喚醒”這個(gè)動(dòng)作上是類似于Monitor.Pulse()和Monitor.PulseAll()的。至于什么時(shí)候類似Pulse(),又在什么時(shí)候類似PulseAll(),往下看。
這個(gè)方法有bool型的返回值:如果該操作成功,則為true;否則,為false。不過(guò)MSDN并沒(méi)有告訴我們,什么時(shí)候執(zhí)行會(huì)失敗,你只有找個(gè)微軟MVP問(wèn)問(wèn)了。
bool:Reset():Sets the state of the event to nonsignaled, causing threads to block. 將事件狀態(tài)設(shè)置為非終止?fàn)顟B(tài),導(dǎo)致線程阻止。 同樣,我們需要明白“nonsignaled”和“非終止”是一回事情。還同樣的是,仍然有個(gè)無(wú)厘頭的返回值。Reset()的作用,相當(dāng)于讓事件重新開(kāi)始處于“進(jìn)行中”,那么此后所有WaitOne()/WaitAll()/WaitAny()/SignalAndWait()這個(gè)事件的線程都會(huì)再次被擋在門外。
來(lái)看看EventWaitHandle眾多構(gòu)造函數(shù)中最簡(jiǎn)單的一個(gè):
EventWaitHandle(Boolean initialState, EventResetMode mode):初始化EventWaitHandle類的新實(shí)例,并指定等待句柄最初是否處于終止?fàn)顟B(tài),以及它是自動(dòng)重置還是手動(dòng)重置。大多數(shù)時(shí)候我們會(huì)在第一個(gè)參數(shù)里使用false,這樣新實(shí)例會(huì)缺省為“非終止”狀態(tài)。第二個(gè)參數(shù)EventResetMode是一個(gè)枚舉,一共兩個(gè)值:
EventResetMode.AutoReset:當(dāng)Set()被調(diào)用當(dāng)前EventWaitHandle轉(zhuǎn)入終止?fàn)顟B(tài)時(shí),若有線程阻塞在當(dāng)前EventWaitHandle上,那么在釋放一個(gè)線程后EventWaitHandle就會(huì)自動(dòng)重置(相當(dāng)于自動(dòng)調(diào)用Reset())再次轉(zhuǎn)入非終止?fàn)顟B(tài),剩余的原來(lái)阻塞的線程(如果有的話)還會(huì)繼續(xù)阻塞。如果調(diào)用Set()后本沒(méi)有線程阻塞,那么EventWaitHandle將保持“終止”狀態(tài)直到一個(gè)線程嘗試等待該事件,這個(gè)該線程不會(huì)被阻塞,此后EventWaitHandle才會(huì)自動(dòng)重置并阻塞那之后的所有線程。
EventResetMode.ManualReset:當(dāng)終止時(shí),EventWaitHandle 釋放所有等待的線程,并在手動(dòng)重置前,即Reset()被調(diào)用前,一直保持終止?fàn)顟B(tài)。
好了,現(xiàn)在我們可以清楚的知道Set()在什么時(shí)候分別類似于Monitor.Pulse()/PulseAll()了:
當(dāng)EventWaitHandle工作在AutoReset模式下,就喚醒功能而言,Set()與Monitor.Pulse()類似。此時(shí),Set()只能喚醒眾多(如果有多個(gè)的話)被阻塞線程中的一個(gè)。但兩者仍有些差別:
Set()的作用不僅僅是“喚醒”而是“釋放”,可以讓線程繼續(xù)工作(proceed);相反,Pulse()喚醒的線程只是重新進(jìn)入Running狀態(tài),參與對(duì)象鎖的爭(zhēng)奪,誰(shuí)都不能保證它一定會(huì)獲得對(duì)象鎖。
Pulse()的已被調(diào)用的狀態(tài)不會(huì)被維護(hù)。因此,如果在沒(méi)有等待線程時(shí)調(diào)用Pulse(),那么下一個(gè)調(diào)用Monitor.Wait()的線程仍然會(huì)被阻塞,就像Pulse() 沒(méi)有被被調(diào)用過(guò)。也就是說(shuō)Monitor.Pulse()只在調(diào)用當(dāng)時(shí)發(fā)揮作用,并不象Set()的作用會(huì)持續(xù)到下一個(gè)WaitXXX()。
在一個(gè)工作在ManualReset模式下的EventWaitHandle的Set()方法被調(diào)用時(shí),它所起到的喚醒作用與Monitor.PulseAll()類似,所有被阻塞的線程都會(huì)收到信號(hào)被喚醒。而兩者的差別與上面完全相同。
來(lái)看看EventWaitHandle的其它構(gòu)造函數(shù):
EventWaitHandle(Boolean initialState, EventResetMode mode, String name):頭兩個(gè)參數(shù)我們已經(jīng)看過(guò),第三個(gè)參數(shù)name用于在系統(tǒng)范圍內(nèi)指定同步事件的名稱。是的,正如我們?cè)贛utex一篇中提到的,由于父類WaitHandle是具有跨進(jìn)程域的能力的,因此跟Mutex一樣,我們可以創(chuàng)建一個(gè)全局的EventWaitHandle,讓后將它用于進(jìn)程間的通知。注意,name仍然是大小寫敏感的,仍然有命名前綴的問(wèn)題跟,你可以參照這里。當(dāng)name為null或空字符串時(shí),這等效于創(chuàng)建一個(gè)局部的未命名的EventWaitHandle。仍然同樣的還有,可能會(huì)因?yàn)橐呀?jīng)系統(tǒng)中已經(jīng)有同名的EventWaitHandle而僅僅返回一個(gè)實(shí)例表示同名的EventWaitHandle。所以最后仍舊同樣地,如果你需要知道這個(gè)EventWaitHandle是否由你最先創(chuàng)建,你需要使用以下兩個(gè)構(gòu)造函數(shù)之一。
EventWaitHandle(Boolean initialState, EventResetMode mode, String name, out Boolean createdNew):createdNew用于表明是否成功創(chuàng)建了EventWaitHandle,true表明成功,false表明已經(jīng)存在同名的事件。
EventWaitHandle(Boolean initialState, EventResetMode mode, String name, out Boolean createdNew, EventWaitHandleSecurity):關(guān)于安全的問(wèn)題,直接查看這個(gè)構(gòu)造函數(shù)上的例子吧。全局MutexEventWaitHandle的安全問(wèn)題應(yīng)該相對(duì)Mutex更需要注意,因?yàn)橛锌赡芎诳统绦蛴孟嗤氖录麑?duì)你的線程發(fā)送信號(hào)或者進(jìn)行組織,那樣可能會(huì)嚴(yán)重危害你的業(yè)務(wù)邏輯。
好啦,都差不多了,可以寫一個(gè)例子試試了。讓我們回到Monitor一篇中提到的生產(chǎn)者和消費(fèi)者場(chǎng)景,讓我們看看EventWaitHandle能不能完成它兄弟Mutex沒(méi)有能完成的事業(yè)。不過(guò),即便有強(qiáng)大通信能力的EventWaitHandle出馬,也避免不要使用lock/monitor或是Mutex。原因很簡(jiǎn)單,糖罐是一個(gè)互斥資源,必須被互斥地訪問(wèn)。而EventWaitHanldle跟Mutex相反,能通信了但卻完全失去了臨界區(qū)的能力。所以,這個(gè)例子其實(shí)并不太適合展示EventWaitHandle的通信機(jī)制,我只是為了想用同樣的例子來(lái)比較這些同步機(jī)制間的差異。
EventWaitHandle雖然還必須借助lock/Monitor/Mutex來(lái)實(shí)現(xiàn)這個(gè)例子(僅僅是臨界區(qū)部分),但是它終究有強(qiáng)于Monitor的通信能力,所以讓我們來(lái)擴(kuò)展一下這個(gè)例子:現(xiàn)在有一個(gè)生產(chǎn)者,有多個(gè)消費(fèi)者。
我們讓消費(fèi)者在沒(méi)有糖吃或吃完一塊糖后阻塞在一個(gè)工作在ManualReset模式下的EventWaitHandle,生產(chǎn)者在生產(chǎn)完畢后就通過(guò)這個(gè)事件喚醒所有消費(fèi)者吃糖。由于我們使用了lock的關(guān)系,雖然所有消費(fèi)者都被喚醒,但是他們還是因?yàn)闋?zhēng)奪糖罐的關(guān)系只有一個(gè)能進(jìn)入臨界區(qū)吃糖。不過(guò)此時(shí)阻塞的原因并不是因?yàn)槲覀兊耐ㄖ獣r(shí)間,而是臨界區(qū)的問(wèn)題。
每個(gè)消費(fèi)者有一條專線,即一個(gè)工作在AutoRest模式下的EventWaitHandle,用于在吃完糖后通知生產(chǎn)者。而生產(chǎn)者用WaitAny()來(lái)等待消費(fèi)者吃糖時(shí)間的發(fā)生,只要有任一消費(fèi)者吃完糖,那么生產(chǎn)者就試圖爭(zhēng)奪對(duì)糖罐的擁有權(quán),把糖罐塞滿(一人一顆的標(biāo)準(zhǔn))。消費(fèi)者這里使用了WaitAndSignal給生產(chǎn)者發(fā)消息,并等待生產(chǎn)者進(jìn)入臨界區(qū)生產(chǎn)糖后通知他們。在這樣的設(shè)計(jì)邏輯下,可能糖罐中的糖還沒(méi)有全部吃完生產(chǎn)者就有機(jī)會(huì)再次把糖罐裝滿。當(dāng)然,你也可以使用了WaitAll()來(lái)等待所有消費(fèi)者吃完再進(jìn)行生產(chǎn)。
轉(zhuǎn)載于:https://www.cnblogs.com/swneng/p/10081210.html
總結(jié)
以上是生活随笔為你收集整理的c#多线程同步之EventWaitHandle使用的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 产品与项目
- 下一篇: 横向技术分析C#、C++和Java优劣