天天看點

【轉】多線程:C#線程同步lock,Monitor,Mutex,同步事件和等待句柄(上)

本篇從Monitor,Mutex,ManualResetEvent,AutoResetEvent,WaitHandler的類關系圖開始,希望通過

本篇的介紹能對常見的線程同步方法有一個整體的認識,而對每種方式的使用細節,适用場合不會過多解釋。讓我們來看看這幾個類的關系圖:

【轉】多線程:C#線程同步lock,Monitor,Mutex,同步事件和等待句柄(上)

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對象變量來保護所有執行個體所共有的資料。

我們再來看看lock關鍵字的本質,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方法模拟小孩吃蛋糕的情景。

C#代碼  

//僅僅是說明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()  

                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)方法

的差別,了解它們的差別很關鍵的一點是要了解同步的對象包含若幹引用,其中包括對目前擁有鎖的線程的引用、對就緒隊列(包含準備擷取鎖的線程)的引用和對

等待隊列(包含等待對象狀态更改通知的線程)的引用。