本篇從Monitor,Mutex,ManualResetEvent,AutoResetEvent,WaitHandler的類別關係圖開始,希 望通過本篇的介紹能對常見的線程同步方法有一個整體的認識,而對每種方式的使用細節,適用場合不會過多解釋。讓我們來看看這幾個類的關係圖:
1.lock關鍵字
lock是C#關鍵詞,它將語句塊標記為臨界區,確保當一個線程位於代碼的臨界區時,另一個線程不進入臨界區。如果其他線程試圖進入鎖定的代碼,則它將一直等待(即被阻止),直到該對象被釋放。方法是擷取給定對象的互斥鎖,執行語句,然後釋放該鎖。
MSDN上給出了使用lock時的注意事項通常,應避免鎖定 public 類型,否則執行個體將超出代碼的控制範圍。常見的結構 lock (this)、lock (typeof (MyType)) 和 lock ("myLock") 違反此準則。
1)如果執行個體可以被公用訪問,將出現 lock (this) 問題。
2)如果 MyType 可以被公用訪問,將出現 lock (typeof (MyType)) 問題由於一個類的所有執行個體都只有一個類型對象(該對象是typeof的返回結果),鎖定它,就鎖定了該對象的所有執行個體。微軟現在建議不要使用 lock(typeof(MyType)),因為鎖定類型對象是個很緩慢的過程,並且類中的其他線程、甚至在同一個應用程式定義域中啟動並執行其他程式都可以訪問 該類型對象,因此,它們就有可能代替您鎖定類型對象,完全阻止您的執行,從而導致你自己的代碼的掛起。
3)由於進程中使用同一字串的任何其他代碼將共用同一個鎖,所以出現 lock(“myLock”) 問題。這個問題和.NET Framework建立字串的機制有關係,如果兩個string變數值都是"myLock",在記憶體中會指向同一字串對象。
最佳做法是定義 private 對象來鎖定, 或 private static物件變數來保護所有執行個體所共有的資料。
我們再來通過IL Dasm看看lock關鍵字的本質,下面是一段簡單的測試代碼:
lock (lockobject)
{
int i = 5;
}
用IL Dasm開啟編譯後的檔案,上面的語句塊產生的IL代碼為:
IL_0045: call void [mscorlib]System.Threading.Monitor::Enter(object)
IL_004a: nop
.try
{
IL_004b: nop
IL_004c: ldc.i4.5
IL_004d: stloc.1
IL_004e: nop
IL_004f: leave.s IL_0059
} // end .try
finally
{
IL_0051: ldloc.3
IL_0052: call void [mscorlib]System.Threading.Monitor::Exit(object)
IL_0057: nop
IL_0058: endfinally
} // end handler
通過上面的代碼我們很清楚的看到:lock關鍵字其實就是對Monitor類的Enter()和Exit()方法的封裝,並通過try...catch...finally語句塊確保在lock語句塊結束後執行Monitor.Exit()方法,釋放互斥鎖。
2.Monitor類
Monitor類通過向單個線程授予對象鎖來控制對對象的訪問。對象鎖提供限制訪問臨界區的能力。當一個線程擁有對象的鎖時,其他任何 線程都不能擷取該鎖。還可以使用 Monitor 來確保不會允許其他任何線程訪問正在由鎖的所有者執行的應用程式代碼節,除非另一個線程正在使用其他的鎖定對象執行該代碼。
通過對lock關鍵字的分析我們知道,lock就是對Monitor的Enter和Exit的一個封裝,而且使用起來更簡潔,因此Monitor類的Enter()和Exit()方法的組合使用可以用lock關鍵字替代。
另外Monitor類還有幾個常用的方法:
TryEnter()能夠有效解決長期死等的問題,如果在一個並發經常發生,而且期間長的環境中使用TryEnter,可以有效防止死結或者長時間 的等待。比如我們可以設定一個等待時間bool gotLock = Monitor.TryEnter(myobject,1000),讓當前線程在等待1000秒後根據返回的bool值來決定是否繼續下面的操作。
Wait()釋放對象上的鎖以便允許其他線程鎖定和訪問該對象。在其他線程訪問對象時,調用線程將等待。脈衝訊號用於通知等待線程有關對象狀態的更改。
Pulse(),PulseAll()向一個或多個等待線程發送訊號。該訊號通知等待線程鎖定對象的狀態已更改,並且鎖的所有者準備釋放該鎖。等待線程被 放置在對象的就緒隊列中以便它可以最後接收對象鎖。一旦線程擁有了鎖,它就可以檢查對象的新狀態以查看是否達到所需狀態。
注意:Pulse、PulseAll和Wait方法必須從同步的代碼塊內調用。
我們假定一種情景:媽媽做蛋糕,小孩有點饞,媽媽每做好一塊就要吃掉,媽媽做好一塊後,告訴小孩蛋糕已經做好了。下面的例子用Monitor類的Wait和Pulse方法類比小孩吃蛋糕的情景。
Wait和Pulse的樣本
//僅僅是說明Wait和Pulse/PulseAll的例子
//邏輯上並不嚴密,使用情境也並不一定合適
class MonitorSample
{
private int n = 1; //生產者和消費者共同處理的資料
private int max = 10000;
private object monitor = new object();
public void Produce()
{
lock (monitor)
{
for (; n <= max; n++)
{
Console.WriteLine("媽媽:第" + n.ToString() + "塊蛋糕做好了");
//Pulse方法不用調用是因為另一個線程中用的是Wait(object,int)方法
//該方法使被阻止線程進入了同步對象的就緒隊列
//是否需要脈衝啟用是Wait方法一個參數和兩個參數的重要區別
//Monitor.Pulse(monitor);
//調用Wait方法釋放對象上的鎖並阻止該線程(線程狀態為WaitSleepJoin)
//該線程進入到同步對象的等待隊列,直到其它線程調用Pulse使該線程進入到就緒隊列中
//線程進入到就緒隊列中才有條件爭奪同步對象的所有權
//如果沒有其它線程調用Pulse/PulseAll方法,該線程不可能被執行
Monitor.Wait(monitor);
}
}
}
public void Consume()
{
lock (monitor)
{
while (true)
{
//通知等待隊列中的線程鎖定對象狀態的更改,但不會釋放鎖
//接收到Pulse脈衝後,線程從同步對象的等待隊列移動到就緒隊列中
//注意:最終能獲得鎖的線程並不一定是得到Pulse脈衝的線程
Monitor.Pulse(monitor);
//釋放對象上的鎖並阻止當前線程,直到它重新擷取該鎖
//如果指定的逾時間隔已過,則線程進入就緒隊列
Monitor.Wait(monitor,1000);
Console.WriteLine("孩子:開始吃第" + n.ToString() + "塊蛋糕");
}
}
}
static void Main(string[] args)
{
MonitorSample obj = new MonitorSample();
Thread tProduce = new Thread(new ThreadStart(obj.Produce));
Thread tConsume = new Thread(new ThreadStart(obj.Consume));
//Start threads.
tProduce.Start();
tConsume.Start();
Console.ReadLine();
}
}
這個例子的目的是要理解Wait和Pulse如何保證線程同步的,同時要注意Wait(obeject)和Wait(object,int)方法的區別,理解它們的區別很關鍵的一點是要理解同步的對象包含若干引用,其中包括對當前擁有鎖的線程的引用、對就緒隊列(包含準備擷取鎖的線程)的引用和對等待隊列(包含等待對象狀態更改通知的線程)的引用。