C#線程同步(2)- 臨界區&Monitor

文章原始出處 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相同的功能:

  • Enter(object)/TryEnter(object)/TryEnter(object, int32)/TryEnter(object, timespan):用來獲取對象鎖(Lock中已經提到過,這裏再強調一次,是對象類型而不能是值類型),標記臨界區的開始。與Enter不一樣,TryEnter永遠不會阻塞代碼,當沒法獲取對象鎖時它會返回False,而且調用者不進入臨界區。TryEnter還有兩種重載,能夠定義一個時間段,在該時間段內一直嘗試得到對象鎖,超時則返回False。
  • Exit(object):沒啥好說的,釋放對象鎖、退出臨界區。只是必定記得在try的finally塊裏調用,不然一但因爲異常形成Exit沒法執行,對象鎖得不到釋放,就會形成死鎖。此外,調用Exit的線程必須擁有 object 參數上的鎖,不然會引起SynchronizationLockException異常。在調用線程獲取指定對象上的鎖後,能夠重複對該對象進行了相同次數的 Exit 和 Enter 調用;若是調用 Exit 與調用 Enter 的次數不匹配,那麼該鎖不會被正確釋放。

  上篇中提到的有關lock的全部使用方法和建議,都適用於它們。

比lock更「高級」的Monitor

  到此爲止,全部見到的仍是咱們在lock中熟悉的東西,再看Monitor的其它方法以前,咱們來看看那老掉牙的「生產者和消費者」場景。試想消費者和生產者是兩個獨立的線程,同時訪問一個容器:

  • 很顯然這個容器是一個臨界資源(你不會問我爲何是顯然吧?),同時只容許一個線程訪問。
  • 生產者往容器裏存放生產好的資源;消費者消費掉容器裏的資源。

  粗看這個場景並無什麼特殊的問題,只要在兩個線程中分別調用兩個方法,這兩個方法內部都用同一把鎖進入臨界區訪問容器便可。但是問題在於:

  • 消費者鎖定容器,進入臨界區後可能發現容器是空的。它能夠退出臨界區,而後下次再盲目地進入碰碰運氣;若是不退出,那麼讓生產者永遠沒法進入臨界區,往容器裏放入資源供消費者消費,從而形成死鎖。
  • 而生產者也可能進入臨界區後,卻發現容器是滿的。結果同樣,直接退出等下次來碰運氣;或者不退出形成死鎖。

  二者選擇直接退出不會引起什麼問題,無非就是可能屢次無功而返。這麼作,你的程序邏輯老是有機會獲得正確執行的,可是效率很低,由於這樣的機制自己是不可控的,業務邏輯是否得以成功執行徹底是隨機的。

  因此咱們須要更有效、更「優雅」的方式:

  • 消費者在進入臨界區發現容器爲空後,當即釋放鎖並把本身阻塞,等待生產者通知,再也不作無謂的嘗試;若是順利消費資源完畢後,主動通知生產者能夠進行生產了,隨後仍然阻塞本身等待生產者通知。
  • 生產者若是發現容器是滿的,那麼當即釋放鎖並阻塞本身,等待消費者在消費完成後喚醒;在生產完畢後,主動給消費者發出通知,隨後也仍然阻塞本身,等待消費者告訴本身容器已經空了。

  在按這個思路寫出Sample Code前,咱們來看Monitor上須要用的其它重要方法:

  • Wait(Object)/Wait(Object, Int32)/Wait(Object, TimeSpan)/Wait(Object, Int32, Boolean)/Wait(Object, TimeSpan, Boolean):  釋放對象上的鎖並阻塞當前線程,直到它從新獲取該鎖。
    1. 這裏的阻塞是指當前線程進入「WaitSleepJoin」狀態,此時CPU再也不會分配給這種狀態的線程CPU時間片,這其實跟在線程上調用Sleep()時的狀態同樣。這時,線程不會參與對該鎖的分配爭奪。
    2. 要打破這種狀態,須要其它擁有該對象鎖的線程,調用下面要講到的Pulse()來喚醒。不過這與,Sleep()不一樣,只有那些由於該對象鎖阻塞的線程纔會被喚醒。此時,線程從新進入「Running」狀態,參與對對象鎖的爭奪。
    3. 強調一下,Wait()其實起到了Exit()的做用,也就是釋放當前所得到的對象鎖。只不過Wait()同時又阻塞了本身。
    4. 咱們還看到Wait()的幾個重載方法。其中第二、3個方法給Wait加上了一個時間,若是超時Wait會返回再也不阻塞,而且能夠根據Wait 方法的返回值,以肯定它是否已在超時前從新獲取鎖。在這種狀況下,其實線程並不須要等待其它線程Pulse()喚醒,至關於Sleep必定時間後醒來。第四、5個方法在第二、3個方法的基礎上加上exitContent參數,咱們暫時不去管它,你能夠詳細參見這裏:http://msdn.microsoft.com/zh-cn/library/79fkfcw1(VS.85).aspx
  • Pulse(object):向阻塞線程隊列(因爲該object而轉入WaitSleepJoin狀態的全部線程,也就是那些執行了Wait(object)的線程,存放的隊列)中第一個線程發信號,該信號通知鎖定對象的狀態已更改,而且鎖的全部者準備釋放該鎖。收到信號的阻塞線程進入就緒隊列中(那些處於Running狀態的線程,能夠被CPU調用運行的線程在這個隊列裏),以便它有機會接收對象鎖。注意,接受到信號的線程只會從阻塞中被喚醒,並不必定會得到對象鎖。
  • PulseAll(object):與Pulse()不一樣,阻塞隊列中的全部線程都會收到信號,並被喚醒轉入Running狀態,即進入就緒隊列中。至於它們誰會幸運的得到對象鎖,那就要看CPU了。
  • 注意:以上全部方法都只能在臨界區內被調用,換句話說,只有對象鎖的得到者可以正確調用它們,不然會引起SynchronizationLockException異常。 

  好了,有了它們咱們就能夠完成這樣的代碼:

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,堅定反對。

  就到這裏吧,好累~

相關文章
相關標籤/搜索