文章原始出處 http://xxinside.blogbus.com/logs/46740731.htmlhtml
預備知識:C#線程同步(1)- 臨界區&Lock程序員
監視器(Monitor)的概念web
能夠在MSDN(http://msdn.microsoft.com/zh-cn/library/ms173179(VS.80).aspx)上找到下面一段話:數據庫
與lock關鍵字相似,監視器防止多個線程同時執行代碼塊。Enter方法容許一個且僅一個線程繼續執行後面的語句;其餘全部線程都將被阻止,直到執行語句的線程調用Exit。這與使用lock關鍵字同樣。事實上,lock 關鍵字就是用Monitor 類來實現的。例如:服務器
lock(x) { DoSomething(); }網絡
這等效於:ide
System.Object obj = (System.Object)x; System.Threading.Monitor.Enter(obj); try { DoSomething(); } finally { System.Threading.Monitor.Exit(obj); }spa
使用 lock 關鍵字一般比直接使用 Monitor 類更可取,一方面是由於 lock 更簡潔,另外一方面是由於 lock 確保了即便受保護的代碼引起異常,也能夠釋放基礎監視器。這是經過 finally 關鍵字來實現的,不管是否引起異常它都執行關聯的代碼塊。.net
這裏微軟已經說得很清楚了,Lock就是用Monitor實現的,二者都是C#中對臨界區功能的實現。用ILDASM打開含有如下代碼的exe或者dll也能夠證明這一點(我並無本身證明):線程
lock (lockobject) { int i = 5; }
反編譯後的的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
Monitor中和lock等效的方法
Monitor是一個靜態類,所以不能被實例化,只能直接調用Monitor上的各類方法來完成與lock相同的功能:
上篇中提到的有關lock的全部使用方法和建議,都適用於它們。
比lock更「高級」的Monitor
到此爲止,全部見到的仍是咱們在lock中熟悉的東西,再看Monitor的其它方法以前,咱們來看看那老掉牙的「生產者和消費者」場景。試想消費者和生產者是兩個獨立的線程,同時訪問一個容器:
粗看這個場景並無什麼特殊的問題,只要在兩個線程中分別調用兩個方法,這兩個方法內部都用同一把鎖進入臨界區訪問容器便可。但是問題在於:
二者選擇直接退出不會引起什麼問題,無非就是可能屢次無功而返。這麼作,你的程序邏輯老是有機會獲得正確執行的,可是效率很低,由於這樣的機制自己是不可控的,業務邏輯是否得以成功執行徹底是隨機的。
因此咱們須要更有效、更「優雅」的方式:
在按這個思路寫出Sample Code前,咱們來看Monitor上須要用的其它重要方法:
好了,有了它們咱們就能夠完成這樣的代碼:
using System; using System.Threading; using System.Collections; using System.Linq; using System.Text;
class MonitorSample
{
//容器,一個只能容納一塊糖的糖盒子。PS:如今MS已經不推薦使用ArrayList,
//支持泛型的List纔是應該在程序中使用的,我這裏偷懶,不想再去寫一個Candy類了。
private ArrayList _candyBox = new ArrayList(1);
private volatile bool _shouldStop = false; //用於控制線程正常結束的標誌
/// <summary>
/// 用於結束Produce()和Consume()在輔助線程中的執行
/// </summary>
public void StopThread()
{
_shouldStop = true;
//這時候生產者/消費者之一可能由於在阻塞中而沒有機會看到結束標誌,
//而另外一個線程順利結束,因此剩下的那個必定長眠不醒,須要咱們在這裏嘗試叫醒它們。
//不過這並不能確保線程能順利結束,由於可能咱們剛剛發送信號之後,線程才阻塞本身。
Monitor.Enter(_candyBox);
try
{
Monitor.PulseAll(_candyBox);
}
finally
{
Monitor.Exit(_candyBox);
}
}
/// <summary>
/// 生產者的方法
/// </summary>
public void Produce()
{
while(!_shouldStop)
{
Monitor.Enter(_candyBox);
try
{
if (_candyBox.Count==0)
{
_candyBox.Add("A candy");
Console.WriteLine("生產者:有糖吃啦!");
//喚醒可能如今正在阻塞中的消費者
Monitor.Pulse(_candyBox);
Console.WriteLine("生產者:趕快來吃!!");
//調用Wait方法釋放對象上的鎖,並使生產者線程狀態轉爲WaitSleepJoin,阻止該線程被CPU調用(跟Sleep同樣)
//直到消費者線程調用Pulse(_candyBox)使該線程進入到Running狀態
Monitor.Wait(_candyBox);
}
else //容器是滿的
{
Console.WriteLine("生產者:糖罐是滿的!");
//喚醒可能如今正在阻塞中的消費者
Monitor.Pulse(_candyBox);
//調用Wait方法釋放對象上的鎖,並使生產者線程狀態轉爲WaitSleepJoin,阻止該線程被CPU調用(跟Sleep同樣)
//直到消費者線程調用Pulse(_candyBox)使生產者線程從新進入到Running狀態,此才語句返回
Monitor.Wait(_candyBox);
}
}
finally
{
Monitor.Exit(_candyBox);
}
Thread.Sleep(2000);
}
Console.WriteLine("生產者:下班啦!");
}
/// <summary>
/// 消費者的方法
/// </summary>
public void Consume()
{
//即使看到結束標緻也應該把容器中的全部資源處理完畢再退出,不然容器中的資源可能就此丟失
//不過這裏_candyBox.Count是有可能讀到髒數據的,好在咱們這個例子中只有兩個線程因此問題並不突出
//正式環境中,應該用更好的辦法解決這個問題。
while (!_shouldStop || _candyBox.Count > 0)
{
Monitor.Enter(_candyBox);
try
{
if (_candyBox.Count==1)
{
_candyBox.RemoveAt(0);
if (!_shouldStop)
{
Console.WriteLine("消費者:糖已吃完!");
}
else
{
Console.WriteLine("消費者:還有糖沒吃,立刻就完!");
}
//喚醒可能如今正在阻塞中的生產者
Monitor.Pulse(_candyBox);
Console.WriteLine("消費者:趕快生產!!");
Monitor.Wait(_candyBox);
}
else
{
Console.WriteLine("消費者:糖罐是空的!");
//喚醒可能如今正在阻塞中的生產者
Monitor.Pulse(_candyBox);
Monitor.Wait(_candyBox);
}
}
finally
{
Monitor.Exit(_candyBox);
}
Thread.Sleep(2000);
}
Console.WriteLine("消費者:都吃光啦,下次再吃!");
}
static void Main(string[] args)
{
MonitorSample ss = new MonitorSample();
Thread thdProduce = new Thread(new ThreadStart(ss.Produce));
Thread thdConsume = new Thread(new ThreadStart(ss.Consume));
//Start threads.
Console.WriteLine("開始啓動線程,輸入回車終止生產者和消費者的工做……\r\n******************************************");
thdProduce.Start();
Thread.Sleep(2000); //儘可能確保生產者先執行
thdConsume.Start();
Console.ReadLine();
//經過IO阻塞主線程,等待輔助線程演示直到收到一個回車
ss.StopThread(); //正常且優雅的結束生產者和消費者線程
Thread.Sleep(1000); //等待線程結束
while (thdProduce.ThreadState != ThreadState.Stopped)
{
ss.StopThread(); //線程尚未結束有多是由於它自己是阻塞的,嘗試使用StopThread()方法中的PulseAll()喚醒它,讓他看到結束標誌
thdProduce.Join(1000); //等待生產這線程結束
}
while (thdConsume.ThreadState != ThreadState.Stopped)
{
ss.StopThread();
thdConsume.Join(1000); //等待消費者線程結束
}
Console.WriteLine("******************************************\r\n輸入回車結束!");
Console.ReadLine();
}
}
可能的幾種輸出(不是所有可能):
開始啓動線程,輸入回車終止生產者和消費者的工做…… ****************************************** 生產者:有糖吃啦! 生產者:趕快來吃!!
消費者:還有糖沒吃,立刻就完! 消費者:趕快生產!! 生產者:下班啦! 消費者:都吃光啦,下次再吃! ****************************************** 輸入回車結束!
開始啓動線程,輸入回車終止生產者和消費者的工做…… ****************************************** 生產者:有糖吃啦! 生產者:趕快來吃!! 消費者:糖已吃完! 消費者:趕快生產!!
生產者:下班啦! 消費者:都吃光啦,下次再吃! ****************************************** 輸入回車結束!
開始啓動線程,輸入回車終止生產者和消費者的工做…… ****************************************** 生產者:有糖吃啦! 生產者:趕快來吃!! 消費者:糖已吃完! 消費者:趕快生產!! 生產者:有糖吃啦! 生產者:趕快來吃!!
消費者:還有糖沒吃,立刻就完! 消費者:趕快生產!! 生產者:下班啦! 消費者:都吃光啦,下次再吃! ****************************************** 輸入回車結束!
有興趣的話你還能夠嘗試修改生產者和消費者的啓動順序,嘗試下其它的結果(好比糖罐爲空)。其實生產者和消費者方法中那個Sleep(2000)也是爲了方便手工嘗試出不一樣分支的執行狀況,輸出中的空行就是我敲入回車讓線程停止的時機。
你可能已經發現,除非消費者先於生產者啓動,不然咱們永遠不會看到消費者說「糖罐是空的!」,這是由於消費者在吃糖之後把本身阻塞了,直到生產者生產出糖塊後喚醒本身。另外一方面,生產者即使先於消費者啓動,在這個例子中咱們也永遠不會看到生產者說「糖罐是滿的!」,由於初始糖罐爲空且生產者在生產後就把本身阻塞了。
題外話1: 是否是以爲生產者判斷糖罐是滿的、消費者檢查出糖罐是空的分支有些多餘? 想一想,若是糖罐初始也許並不爲空,又或者消費者先於生產者執行,那麼它們就會派上用場。這畢竟只是一個例子,咱們在沒有任何限制條件下設計了這個環環相扣的簡單場景,因此讓這兩個分支「顯得」有些多餘,但大多數真實狀況並不如此。 在實際應用中,生產者每每表明負責從某處簡單接收資源的線程,好比來自網絡的指令、從服務器返回的查詢等等;而消費者線程須要負責解析指令、解析返回的查詢結果,而後存儲到本地數據庫、文件或者呈現給用戶等等。消費者線程的任務每每更復雜,執行時間更長,爲了提升程序的總體執行效率,消費者線程每每會多於生產者線程,可能3對1,也可能5對2…… CPU的隨機調度,可能會形成各類各樣的狀況。你基本上是沒法預測一段代碼在被調用時,與之相關的外部環境是怎樣的,因此完備的處理每個分支是必要的。 另外一方面,即使一個分支的狀況不是咱們設計中指望發生的,可是因爲某種如今沒法預見的錯誤,形成本「不可能」、「不該該」出現的分支得以執行,那麼在這個分支的代碼能夠保障你的業務邏輯能夠在錯誤的異常狀況下得以修正,至少你也能夠報警避免更大的錯誤。 因此老是建議給每一個if都寫上else分支,這除了讓你的代碼顯得更加僅僅有條、邏輯清晰外,還可能給你帶來額外的擴展性和健壯性。就像在前一篇中所提到的,不要由於別人(你所寫類的使用者)的「錯誤」(誰讓你給別人這個機會呢?)連累本身!
題外話2:
你能夠用微軟的建議用
lock(_candyBox){...}
替代上面代碼中的
Monitor.Enter(_candyBox);
try{...}
finally
{
Monitor.Exit(_candyBox);
},這裏我不作任何反對。不過在更多時候,你核能會須要在finally裏作更多的事情,而不僅是Exit那麼簡單,因此即使用了lock,你還得本身寫try/finally。
若是你的頭已經有些暈了,那麼立刻跳過這個題外話,下面說的跟線程同步毫無關係。這個題外話其實想引伸到using。這個C#特有的(其它.net語言沒有相似語法)關鍵字,它會幫你自動調用全部實現了IDisposable接口類上的Dispose()方法。跟lock相似,using(obj) {//do something}等效於一個以下的try/finally語句塊:
SS obj = new SS();
try
{
//use obj to do something
}
finally
{
obj.Dispose();
}
微軟一廂情願的但願經過using避免程序員忘記調用Dispose()去釋放該類所佔用的那些資源,包括託管的和非託管的(磁盤IO、網絡IO、數據庫鏈接IO等等),你一般會在關於磁盤操做的類、各類Stream、網絡操做相關的類、數據庫驅動類上找到這個方法。Dispose()裏主要是替你Disconnet()/Close()掉這些資源,可是這些Dispose()方法經常是由微軟以外的公司編寫的,好比Oracle的.Net驅動。你能確信Oracle的程序員很是瞭解Dispose()在.net中的重要含義麼?回頭來講,就算是微軟本身的程序員,難道就不會犯錯誤嗎?跟lock中提到的SynRoot實現同樣,你根本不知道你所使用類的Dispose()是不是正確的,也沒法確保下一個版本的Dispose()不會悄悄的改變……對於這些敏感的資源,本身老老實實去Disconnect()/Close(),再老老實實的去Dispose()。事實上finally須要作的事情也每每不僅是一個Dispose()。 一句話,關於using,堅定反對。
就到這裏吧,好累~