转:C# 线程同步技术 Monitor 和Lock
原文地址:http://www.cnblogs.com/lxblog/archive/2013/03/07/2947182.html
今天我們總結一下 C#線程同步 中的 Monitor 類 和 Lock 關鍵字進行一下總結。
首先來看看他們有什么異同(相信對此熟悉的朋友們都很清楚):
| 1、他們都是在指定對象上獲取排他鎖,用于同步代碼區 lock(obj){ |
?
所以lock能做的,Monitor肯定能做,Monitor能做的,lock不一定能做,我們今天就主要說的就是Monitor 類。
Monitor 類 通過Enter(Object) 在指定對象上獲取排他鎖,通過Exit 方法釋放指定對象上的排他鎖。
Enter方法:使用 Enter 獲取作為參數傳遞的對象上的 Monitor。如果其他線程已對該對象執行了 Enter,但尚未執行對應的 Exit,則當前線程將阻止,直到對方線程釋放該對象
Exit方法:調用線程必須擁有 obj 參數上的鎖。如果調用線程擁有指定對象上的鎖并為該對象進行了相同次數的 Exit 和 Enter 調用,則該鎖將被釋放。如果調用線程調用 Exit 與調用 Enter 的次數不同,則該鎖不會被釋放。
我們來做一個游戲殺怪的例子來演示一下吧:建立一個控制臺程序,并增加一個怪物類(Monster),代碼如下:?
public class Monster {public Monster(int blood){this.Blood = blood; Console.WriteLine(string.Format("我是怪物,我有 {0} 滴血!\r\n", blood));}public int Blood { get; set; } }?然后呢,我們在增加一個Player 類,里面有個物理工具的方法,此方法沒有采取任何線程同步的措施:?
public class Player{//姓名public string Name { get; set; }//武器public string Weapon { get; set; }//攻擊力public int Power { get; set; }//物理攻擊public void PhysAttack(Object monster){Monster m = monster as Monster;while (m.Blood > 0){Console.WriteLine("當前玩家 【{0}】,使用{1}攻擊怪物!", this.Name, this.Weapon);if (m.Blood >= this.Power){m.Blood -= this.Power;}else{m.Blood = 0;} Console.WriteLine("怪物剩余血量:{0}\r\n", m.Blood);} }?在主函數中,我們實例化兩個玩家角色,一個游俠,一個野蠻人,并開啟兩個線程來調用一下他們的物理攻擊方法,攻擊同一個怪物。
static void Main(string[] args) { Monster monster = new Monster(1000);Player YouXia = new Player() { Name = "游俠", Weapon = "寶劍", Power = 150 };Player YeManRen = new Player() { Name = "野蠻人", Weapon = "鏈錘", Power = 250 };Thread t1 = new Thread(new ParameterizedThreadStart(YouXia.PhysAttack));t1.Start(monster);Thread t2 = new Thread(new ParameterizedThreadStart(YeManRen.PhysAttack));t2.Start(monster);t1.Join();t2.Join();Console.ReadKey();}??由于沒有采取線程同步的措施,運行結果可想而知,當然不同的計算機運行結果是不一樣的,我的如下圖:
這種結果肯定不是我們想要的,我們來對Player 類中的物理攻擊方法,修改一下,用Monitor 類來實現一下線程同步,當然也可以用Lock 關鍵字,修改的代碼如下:
//物理攻擊public void PhysAttack(Object monster) {Monster m = monster as Monster;while (m.Blood > 0) //異步讀{Monitor.Enter(monster);if (m.Blood > 0) //同步讀{Console.WriteLine("當前玩家 【{0}】,使用{1}攻擊怪物!", this.Name, this.Weapon);if (m.Blood >= this.Power){m.Blood -= this.Power;}else{m.Blood = 0;}Console.WriteLine("怪物剩余血量:{0}\r\n", m.Blood);}Thread.Sleep(500);Monitor.Exit(monster);}
}
由于我們加上了Monitor.Enter(monster) 和?Monitor.Exit(monster); 期間的代碼段是線程同步的。假如程序啟動后,游俠所在的線程 先進入了Monitor.Enter(monster),這時候游俠線程擁有對monster實例的排他鎖,其他的線程必須等待,野蠻人線程運行到Monitor.Enter(monster)的時候,就會發生阻塞,直到游俠線程執行?Monitor.Exit(monster);之后,釋放了排他鎖,野蠻人線程才能進行殺怪的操作,此時野蠻人線程擁有排他鎖的控制權,游俠線程就必須等待。運行結果如下:
?將上面的代碼中的?Monitor.Enter(monster); 和?Monitor.Exit(monster); 替換成lock(monster);是會得到同樣的效果的,那么我們再來看lock 沒有的功能。Monitor類中的Wait(object) 和Pulse 方法。
?Wait(object)方法:釋放對象上的鎖并阻止當前線程,直到它重新獲取該鎖,該線程進入等待隊列。
?Pulse方法:只有鎖的當前所有者可以使用?Pulse?向等待對象發出信號,當前擁有指定對象上的鎖的線程調用此方法以便向隊列中的下一個線程發出鎖的信號。接收到脈沖后,等待線程就被移動到就緒隊列中。在調用?Pulse?的線程釋放鎖后,就緒隊列中的下一個線程(不一定是接收到脈沖的線程)將獲得該鎖。
另外:Wait 和 Pulse 方法必須寫在?Monitor.Enter 和Moniter.Exit 之間。
不明白MSDN的解釋,沒有關系,不明白什么是等待隊列和就緒隊列也沒有關系,來繼續我們的實例。為了好演示,為Player類又增加兩方法一個是 魔法攻擊,一個是閃電攻擊,兩者的代碼是一樣的,只不過分別加上了Monitor.Wait 和 Monitor.Exit 方法。
//魔法攻擊public void MigcAttack(Object monster){Monster m = monster as Monster;Monitor.Enter(monster);Console.WriteLine("當前玩家 {0} 進入戰斗\r\n",this.Name);while (m.Blood > 0){Monitor.Wait(monster);Console.WriteLine("當前玩家 {0} 獲得攻擊權限", this.Name);Console.WriteLine("當前玩家 {0},使用 魔法 攻擊怪物!", this.Name, this.Weapon);m.Blood = (m.Blood >= this.Power) ? m.Blood - this.Power : 0;Console.WriteLine("怪物剩余血量:{0}\r\n", m.Blood);Thread.Sleep(500);Monitor.Pulse(monster);}Monitor.Exit(monster);}//閃電攻擊public void LightAttack(Object monster){Monster m = monster as Monster; Monitor.Enter(monster);Console.WriteLine("當前玩家 {0} 進入戰斗\r\n", this.Name);while (m.Blood > 0){Monitor.Pulse(monster);Console.WriteLine("當前玩家 {0} 獲得攻擊權限", this.Name);Console.WriteLine("當前玩家 {0},使用 閃電 攻擊怪物!", this.Name);m.Blood = (m.Blood >= this.Power) ? m.Blood - this.Power : 0;Console.WriteLine("怪物剩余血量:{0}\r\n", m.Blood);Thread.Sleep(500);Monitor.Wait(monster);}Monitor.Exit(monster);}?并在Main方法中開兩個線程進行調用:
static void Main(string[] args) {Monster monster = new Monster(1500);Player Cike = new Player() { Name = "刺客", Power = 250 };Player Mofashi = new Player() { Name = "魔法師", Power = 350 };Thread t1 = new Thread(new ParameterizedThreadStart(Cike.LightAttack));t1.Start(monster);Thread t2 = new Thread(new ParameterizedThreadStart(Mofashi.MigcAttack));t2.Start(monster);
t1.Join();t2.Join();Console.ReadKey(); }
?先不看上面代碼的對與錯,我們先認為理論上是正確的。
我們分析一下:有這樣一種可能,程序運行后,魔法師線程先進入了 Monitor.Enter(monster), 獲得了對monser實例的排他鎖控制權,然后魔法師線程繼續運行,當運行到了Monitor.Wait(monster)的時候,發生了阻塞,魔法師線程釋放了排他鎖的控制權,進入了等待隊列。
這時候,刺客線程才剛剛獲得CPU分給的時間片剛剛運行,由于魔法師線程已經釋放了排他鎖,因此刺客線程順利的進入了Monitor.Enter(monster),并獲得了對monser實例的排他鎖控制權,然后 運行到了Monitor.Pulse(monster); 發送了個Pulse信號,告訴魔法師線程,你就緒吧,等我進入Wait之后,你可以殺怪了。
因此刺客線程運行到Wait 之后,魔法師線程可以繼續運行。這樣兩線程的 Wait 和 Pulse 就形成了一個循環,就會出現,刺客用閃電攻擊一次,魔法師用魔法攻擊一次的情況,直到怪物被干掉。
結果如下圖:
若沒有出現上面的結果 (多運行幾次,總會有機會出現的)。
不過總是有些幸運的人一運行就會出現如下的結果:
程序運行到這里,不動了....,哈哈哈恭喜你,這就是發生了死鎖。
我們也來分析一下:
怪物出場后,刺客線程一馬當先的進入了殺怪過程,先進入了Monitor.Enter(monster),又發送了Monitor.Pulse(monster),不過此時的沒有任何等待線程(白玩),刺客進行閃電攻擊后,遇到了Wait 方法,交出了 排他鎖控制權,然后去一邊兒等待去了。
此時的 魔法師線程才剛剛開始運行,進入了Monitor.Enter(monster),獲得排他鎖控制權,還沒有出招,就碰到了Wait 方法,結果是 也交出了排他鎖,去一邊兒等待去了,我們的程序就兩個角色線程,都去等待去了,不發生死鎖才怪!
如何解決上面的問題呢?我們可以采用Wait(object)的一個重載方法?Wait(Object, Int32) 方法。
bool Wait(Object, Int32):釋放對象上的鎖并阻止當前線程,直到它重新獲取該鎖。?如果指定的超時間隔已過,則線程進入就緒隊列。
結合下面的代碼給大家通俗的解釋就是:Int32 是一個毫秒數;該方法釋放排他鎖,阻塞當前線程,如果在規定的毫秒數內獲得是鎖的控制權,就返回True, 該線程繼續運行; 否則就返回False,該線程也繼續運行。
來修改一下上面的代碼,將魔法攻擊和閃電攻擊代碼修改如下:?
//魔法攻擊 public void MigcAttack(Object monster) {Monster m = monster as Monster;Monitor.Enter(monster);Console.WriteLine("當前玩家【{0}】進入戰斗\r\n",this.Name);while (m.Blood > 0){Monitor.Wait(monster);Console.WriteLine("當前玩家【{0}】獲得攻擊權限", this.Name);if (m.Blood > 0){Console.WriteLine("當前玩家【{0}】,使用 魔法 攻擊怪物!", this.Name, this.Weapon);m.Blood = (m.Blood >= this.Power) ? m.Blood - this.Power : 0;Console.WriteLine("怪物剩余血量:{0}\r\n", m.Blood);}else{Console.WriteLine("怪物倒下了! 【{0}】停止了魔法攻擊 \r\n", this.Name);}Thread.Sleep(500);Monitor.Pulse(monster);}Monitor.Exit(monster);}//閃電攻擊 public void LightAttack(Object monster) {Monster m = monster as Monster; Monitor.Enter(monster);Console.WriteLine("當前玩家【{0}】進入戰斗\r\n", this.Name);while (m.Blood > 0){Monitor.Pulse(monster);if (Monitor.Wait(monster, 1000)) //主要是這里{Console.WriteLine("當前玩家【{0}】獲得攻擊權限", this.Name);if (m.Blood > 0){Console.WriteLine("當前玩家【{0}】,使用 閃電 攻擊怪物!", this.Name);m.Blood = (m.Blood >= this.Power) ? m.Blood - this.Power : 0;Console.WriteLine("怪物剩余血量:{0}\r\n", m.Blood);}else{Console.WriteLine("怪物倒下了! 【{0}】停止了閃電攻擊 \r\n", this.Name);}Thread.Sleep(500);}//Monitor.Wait(monster,1000);}Monitor.Exit(monster); }
?由于我們 使用?Monitor.Wait(monster, 1000)修改了?閃電攻擊的方法。當刺客線程進入Wait 的時候,我們只讓該線程等待1s ,如果 1s 內 獲取到了魔法師線程的pusle脈沖信號并獲取到了鎖控制權,刺客線程就可以進行閃電攻擊,如果1s 后,還沒有獲取到控制權,刺客線程繼續運行。總有那么一個時刻魔法師線程進行了等待,刺客線程運行到Pulse 之后,就會通知魔法師線程就緒,再執行到Monitor.Wait(monster, 1000)的時候,魔法師線程進行魔法攻擊。如果魔法師線程1s內攻擊完成,并運行到Wait的時候。刺客線程可以進入if 進行閃電攻擊,如果超時,刺客線程進行循環。
運行結果如下:
通過Monitor.Wait(monster, 1000),我們成功的避免了死鎖的發生,我們再來看看?Monitor.TryEnter(Object) 的使用,該方法也能夠避免死鎖的發生,我們下面的例子用到的是該方法的重載,Monitor.TryEnter(Object,Int32)。
Bool Monitor.TryEnter(Object,Int32):在指定的毫秒數內嘗試獲取指定對象上的排他鎖。如果在指定的毫秒數內獲得排他鎖,則返回True,否則返回False。
同樣結合下面的代碼,Int32 是一個毫秒數;該方法嘗試去獲得排他鎖,阻塞當前線程,如果在規定的毫秒數內獲得是鎖的控制權,就返回True, 該線程繼續運行; 否則就返回False,該線程也繼續運行。
為了說明情況,我們新建一個簡單的計算類,包含加法和減法兩個操作:
public class Calculate {public void Add(){while (true){ if (Monitor.TryEnter(this,1000)) //注意這里,如果1s內獲得鎖,則進入if,超時后進入else{Console.ForegroundColor = ConsoleColor.Red;Console.WriteLine(string.Format("線程{0}獲得鎖:進入了加法運算",Thread.CurrentThread.Name));Console.WriteLine("開始加法運算 1s 鐘");Thread.Sleep(1000);Console.WriteLine(string.Format("線程{0}釋放鎖:離開了加法運算\r\n",Thread.CurrentThread.Name));Monitor.Exit(this);}else{Console.ForegroundColor = ConsoleColor.Red;Console.WriteLine("\r\n 由于減法運算未完成,未進入加法運算"); } }}public void Sub(){while (true){Monitor.Enter(this);Console.ForegroundColor = ConsoleColor.Blue;Console.WriteLine(string.Format("線程{0}獲得鎖:進入了減法運算", Thread.CurrentThread.Name));Console.ForegroundColor = ConsoleColor.Blue;Console.WriteLine("開始減法運算 2s 鐘");Thread.Sleep(2000); //讓減法運算長一點,可以演示效果Console.ForegroundColor = ConsoleColor.Blue;Console.WriteLine(string.Format("線程{0}釋放鎖:離開了減法運算\r\n", Thread.CurrentThread.Name));Monitor.Exit(this);Thread.Sleep(2000);}} }?在Main方法中進行調用:?
static void Main(string[] args) {Calculate c = new Calculate();Thread t1 = new Thread(new ThreadStart(c.Sub));t1.Name = "減法線程";Thread t2 = new Thread(new ThreadStart(c.Add));t2.Name = "加法線程";t1.Start();t2.Start();t1.Join();t2.Join();Console.ReadKey();
}
?由于我們的代碼中兩個方法均用到的是 While(true), 所以兩個線程會不停的運行下去,但不會發生死鎖。結果如下圖:
?今天主要總結了 Monitor 類的一些使用方法,希望大家能看明白。另外 Monitor 是很容易產生死鎖的類,我們平時可以通過 Wait(object,int32) 方法 和 TryEnter() 方法來解決。
轉載于:https://www.cnblogs.com/jearay/p/3668763.html
總結
以上是生活随笔為你收集整理的转:C# 线程同步技术 Monitor 和Lock的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 在word 2010中采用EndNote
- 下一篇: HDU 1850 Being a Goo