1.Monitor.Wait方法
當線程調用 Wait 時,它釋放對象的鎖並進入對象的等待隊列,對象的就緒隊列中的下一個線程(如果有)獲取鎖並擁有對對象的獨占使用。Wait()就是交出鎖的使用權,使線程處於阻塞狀態,直到再次獲得鎖的使用權。
2.Monitor.Pulse方法
當前線程調用此方法以便向隊列中的下一個線程發出鎖的信號。接收到脈沖后,等待線程就被移動到就緒隊列中。在調用 Pulse 的線程釋放鎖后,就緒隊列中的下一個線程(不一定是接收到脈沖的線程)將獲得該鎖。pulse()並不會使當前線程釋放鎖。
簡述:
共用同一lock對象兩線程不能只調用Wait(),Wait這個方法反而放棄了鎖的使用權,同時阻塞當前線程,線程就直接休眠(進入WaitSleepJoin狀態),同時在主線程中Join這個work線程時,也就一直不能返回了。線程將一直阻塞。
讓我們首先看看MSDN對Monitor.Wait的解釋(鏈接見注釋):
釋放對象上的鎖並阻止當前線程,直到它重新獲取該鎖。...
該解釋的確很粗糙,很難理解。讓我們來看看它下面的備注:
同步的對象包含若干引用,其中包括對當前擁有鎖的線程的引用、對就緒隊列的引用和對等待隊列的引用。
這個多少還給了點東西,現在我們腦海中想像這么一幅圖畫:
- Assembly code
-
|- 擁有鎖的線程 lockObj->|- 就緒隊列(ready queue) |- 等待隊列( wait queue)
當一個線程嘗試着lock一個同步對象的時候,該線程就在就緒隊列中排隊。一旦沒人擁有該同步對象,就緒隊列中的線程就可以占有該同步對象。這也是我們平時最經常用的lock方法。
為了其他的同步目的,占有同步對象的線程也可以暫時放棄同步對象,並把自己流放到等待隊列中去。這就是Monitor.Wait。由於該線程放棄了同步對象,其他在就緒隊列的排隊者就可以進而擁有同步對象。
比起就緒隊列來說,在等待隊列中排隊的線程更像是二等公民:他們不能自動得到同步對象,甚至不能自動升艙到就緒隊列。而Monitor.Pulse的作用就是開一次門,使得一個正在等待隊列中的線程升艙到就緒隊列;相應的Monitor.PulseAll則打開門放所有等待隊列中的線程到就緒隊列。
比如下面的程序:
- C# code
-
class Program { static void Main( string [] args) { new Thread(A).Start(); new Thread(B).Start(); new Thread(C).Start(); Console.ReadLine(); } static object lockObj = new object (); static void A() { lock (lockObj) // 進入就緒隊列 { Thread.Sleep( 1000 ); Monitor.Pulse(lockObj); Monitor.Wait(lockObj); // 自我流放到等待隊列 } Console.WriteLine( " A exit... " ); } static void B() { Thread.Sleep( 500 ); lock (lockObj) // 進入就緒隊列 { Monitor.Pulse(lockObj); } Console.WriteLine( " B exit... " ); } static void C() { Thread.Sleep( 800 ); lock (lockObj) // 進入就緒隊列 { } Console.WriteLine( " C exit... " ); } }
從時間線上來分析:
- Assembly code
-
T 線程A 0 lock ( lockObj ) 1 { 2 //... 線程B 線程C 3 //... lock ( lockObj ) lock ( lockObj ) 4 //... { { 5 //... //... 6 //... //... 7 Monitor .Pulse //... 8 Monitor . Wait //... 9 //... Monitor .Pulse 10 //... } } 11 } 時間點0,假設線程A先得到了同步對象,它就登記到同步對象lockObj的“擁有者引用”中。時間點3,線程B和C要求擁有同步對象,他們將在“就緒隊列”排隊: |--(擁有鎖的線程) A | 3 lockObj--|--(就緒隊列) B,C | |--(等待隊列) 時間點7,線程A用Pulse發出信號,允許第一個正在 " 等待隊列 " 中的線程進入到”就緒隊列“。但由於就緒隊列是空的,什么事也沒有發生。時間點8,線程A用Wait放棄同步對象,並把自己放入 " 等待隊列 " 。B,C已經在就緒隊列中,因此其中的一個得以獲得同步對象(假定是B)。B成了同步 對象的擁有者。C現在還是候補委員,可以自動獲得空缺。而A則被關在門外,不能自動獲得空缺。 |--(擁有鎖的線程) B | 8 lockObj--|--(就緒隊列) C | |--(等待隊列) A 時間點9,線程B用Pulse發出信號開門,第一個被關在門外的A被允許放入到就緒隊列,現在C和A都成了候補委員,一旦同步對象空閑,都有機會得它。 |--(擁有鎖的線程) B | 9 lockObj--|--(就緒隊列) C,A | |--(等待隊列) 時間點10,線程B退出Lock區塊,同步對象閑置,就緒隊列隊列中的C或A就可以轉正為擁有者(假設C得到了同步對象)。 |--(擁有鎖的線程) C | 10 lockObj--|--(就緒隊列) A | |--(等待隊列) 隨后C也退出Lock區塊,同步對象閑置,A就重新得到了同步對象,並從Monitor.Wait中返回... 最終的執行結果就是: B exit... C exit... A exit...
- C# code
-
class MyManualEvent { private object lockObj = new object (); private bool hasSet = false ; public void Set() { lock (lockObj) { hasSet = true ; Monitor.PulseAll(lockObj); } } public void WaitOne() { lock (lockObj) { while ( ! hasSet) { Monitor.Wait(lockObj); } } } } class Program { static MyManualEvent myManualEvent = new MyManualEvent(); static void Main( string [] args) { ThreadPool.QueueUserWorkItem(WorkerThread, " A " ); ThreadPool.QueueUserWorkItem(WorkerThread, " B " ); Console.WriteLine( " Press enter to signal the green light " ); Console.ReadLine(); myManualEvent.Set(); ThreadPool.QueueUserWorkItem(WorkerThread, " C " ); Console.ReadLine(); } static void WorkerThread( object state) { myManualEvent.WaitOne(); Console.WriteLine( " Thread {0} got the green light... " , state); } }
我們看到了該玩具MyManualEvent實現了類庫中的ManulaResetEvent的功能,但卻更加的輕便 - 類庫的ManulaResetEvent使用了操作系統內核事件機制,負擔比較大(不算競態時間,ManulaResetEvent是微秒級,而lock是幾十納秒級)。
例子的WaitOne中先在lock的保護下判斷是否信號綠燈,如果不是則進入等待。因此可以有多個線程(比如例子中的AB)在等待隊列中排隊。
當調用Set的時候,在lock的保護下信號轉綠,並使用PulseAll開門放狗,將所有排在等待隊列中的線程放入就緒隊列,A或B(比如A)於是可以重新獲得同步對象,從Monitor.Wait退出,並隨即退出lock區塊,WaitOne返回。隨后B或A(比如B)重復相同故事,並從WaitOne返回。
線程C在myManualEvent.Set()后才執行,它在WaitOne中確信信號燈早已轉綠,於是可以立刻返回並得以執行隨后的命令。
該玩具MyManualEvent可以用在需要等待初始化的場合,比如多個工作線程都必須等到初始化完成后,接到OK信號后才能開工。該玩具MyManualEvent比起ManulaResetEvent有很多局限,比如不能跨進程使用,但它演示了通過基本的Monitor命令組合,達到事件機的作用。
現在是回答朋友們的疑問的時候了:
Q: Lock關鍵字不是有獲取鎖、釋放鎖的功能... 為什么還需要執行Pulse?
A: 因為Wait和Pulse另有用途。
Q: 用lock 就不要用monitor了(?)
A: lock只是Monitor.Enter和Monitor.Exit,用Monitor的方法,不僅能用Wait,還可以用帶超時的Monitor.Enter重載。
Q: Monitor.Wait完全沒必要 (?)
A: Wait和Pulse另有用途。
Q: 什么Pulse和Wait方法必須從同步的代碼塊內調用?
A: 因為Wait的本意就是“[暫時]釋放對象上的鎖並阻止當前線程,直到它重新獲取該鎖”,沒有獲得就談不到釋放。
我們知道lock實際上一個語法糖糖,C#編譯器實際上把他展開為Monitor.Enter和Monitor.Exit,即:
- C# code
-
lock (lockObj) { // ... } /// /相當於(.Net4以前): Monitor.Enter(lockObj); try { // ... } finally { Monitor.Exit(lockObj); }
但是,這種實現邏輯至少理論上有一個錯誤:當Monitor.Enter(lockObj);剛剛完成,還沒有進入try區的時候,有可能從其他線程發出了Thread.Abort等命令,使得該線程沒有機會進入try...finally。也就是說lockObj沒有辦法得到釋放,有可能造成程序死鎖。這也是Thread.Abort一般被認為是邪惡的原因之一。
DotNet4開始,增加了Monitor.Enter(object,ref bool)重載。而C#編譯器會把lock展開為更安全的Monitor.Enter(object,ref bool)和Monitor.Exit:
- C# code
-
lock (lockObj) { // ... } /// /相當於(DotNet 4): bool lockTaken = false ; try { Monitor.Enter(lockObj, ref lockTaken); // } finally { if (lockTaken) Monitor.Exit(lockObj); }
現在Monitor.TryEnter在try的保護下,“加鎖”成功意味着“放鎖”將得到finally的保護。
注釋和引用:
Monitor.Wait 方法
http://msdn.microsoft.com/zh-cn/library/79fkfcw1.aspx
Monitor.TryEnter 方法
http://msdn.microsoft.com/zh-cn/library/dd289679.aspx
請問,多線程Monitor類
http://topic.csdn.net/u/20111206/15/744c70de-49dc-4694-a09e-180438d7f8f0.html
請問,這個關於多線程的代碼不懂
http://topic.csdn.net/u/20111208/23/64671dd4-7fdc-4d76-b3b9-1fd18087e6e0.html