多執行緒中的鎖系統(一)-基礎用法

蘑菇先生發表於2014-12-21

     平常在多執行緒開發中,總避免不了執行緒同步。本篇對net多執行緒中的鎖系統做個簡單描述。

閱讀目錄:

  1. lock、Monitor
  2. 作用域範圍
  3. 字串鎖
  4. Monitor的用法
  5. Mutex
  6. Semaphore
  7. 總結

lock、Monitor

Lock是Monitor語法糖簡化寫法,Lock在IL會生成Monitor。

       //======Example 1=====
            string obj = "helloworld";
            lock (obj)
            {
                Console.WriteLine(obj);
            }
            //lock  IL會編譯成如下寫法
            bool isGetLock = false;
            Monitor.Enter(obj, ref isGetLock);
            try
            {
                Console.WriteLine(obj);
            }
            finally
            {
                if (isGetLock)
                {
                    Monitor.Exit(obj);
                }
            }

isGetLock引數是Framework  4.0後新加的。 為了使程式在所有情況下都能夠確定,是否有必要釋放鎖。例: Monitor.Enter拿不到鎖

Monitor.Enter 是可以鎖值型別的。鎖時會裝箱成新物件,所以無法做到執行緒同步。

作用域範圍

     一:Lock是隻能在程式內鎖,不能跨程式,內部走的是混合構造,先自旋再轉成核心構造。

     二:關於對type型別的鎖,如下:

   //======Example 2=====
            new Thread(new ThreadStart(() => {
                lock (typeof(int))
                {
                    Thread.Sleep(10000);
                    Console.WriteLine("Thread1釋放");
                }
            })).Start();
            Thread.Sleep(1000);
            lock(typeof(int))
            {
                Console.WriteLine("Thread2釋放");
            }

執行結果如下:

在看個例子:

  //======Example 3=====
            Console.WriteLine(DateTime.Now);
            AppDomain appDomain1 = AppDomain.CreateDomain("AppDomain1");
            LockTest Worker1 = (LockTest)appDomain1.CreateInstanceAndUnwrap(
             Assembly.GetExecutingAssembly().FullName,
             "ConsoleApplication1.LockTest");
            Worker1.Run();

            AppDomain appDomain2 = AppDomain.CreateDomain("AppDomain2");
            LockTest Worker2 = (LockTest)appDomain2.CreateInstanceAndUnwrap(
            Assembly.GetExecutingAssembly().FullName,
            "ConsoleApplication1.LockTest");
            Worker2.Run();
/// <summary>
    /// 跨應用程式域邊界或遠端訪問時需要繼承MarshalByRefObject
    /// </summary>
    public class LockTest : MarshalByRefObject
    {
        public void Run()
        {
            lock (typeof(int))
            {
                Thread.Sleep(10000);
                Console.WriteLine(AppDomain.CurrentDomain.FriendlyName + ": Thread 釋放," + DateTime.Now);
            }
        }
    }

執行結果如下:

第一個例子說明,在同程式同域,不同執行緒下,鎖type int,其實鎖的是同一個int物件,所以要慎用。

第二個例子,這裡就簡單說下。

      A: CLR啟動時,會建立 系統域(System Domain)和共享域(Shared Domain), 預設程式域(Default AppDomain)。 系統域和共享域是單例的。程式域可以有多個,例子中我們使用AppDomain.CreateDomain方法建立的。

      B:  按正常來說,每個程式域的程式碼都是隔離,互不影響的。但對於一些基礎類型來說,每個程式域都重新載入一份,就顯得有點浪費,帶來額外的損耗壓力。聰明的CLR會把一些基本型別Object, ValueType, Array, Enum, String, and Delegate等所在的程式集MSCorLib.dll,在CLR啟動過程中都會載入到共享域。  每個程式域都會使用共享域的基礎型別例項。  

      C: 而每個程式域都有屬於自己的託管堆。託管堆中最重要的是GC heap和Loader heap。GC heap用於引用型別例項的儲存,生命週期管理和垃圾回收。Loader heap儲存型別系統,如MethodTable,資料結構等,Loader heap生命週期不受GC管理,跟程式域解除安裝有關。

     所以共享域中Loader heap MSCorLib.dll中的int例項會一直保留著,直到程式結束。單個程式域解除安裝也不受影響。作用域很大有沒有!!!

     這時第二個例子也很容易理解了。 鎖int例項是跨程式域的,MSCorLib中的基礎型別都是這樣, 極容易造成死鎖。  而自定義型別則會載入到自己的程式域,不會影響其他。

字串的鎖

我們都知道鎖的目的,是為了多執行緒下值被破壞。也知道string在c#是個特殊物件,值是不變的,每次變動都是一個新物件值,這也是推薦stringbuilder原因。如例:

    //======Example 4=====
            string str1 = "mushroom";
            string str2 = "mushroom";
            var result1 = object.ReferenceEquals(str1, str2);
            var result2 = object.ReferenceEquals(str1, "mushroom");
            Console.WriteLine(result1 + "-" + result2);
            /* output
             * True-True
             */

 正是由於c#中字串的這種特性,所以字串是在多執行緒下是不會被修改的,只讀的。它存在於SystemDomain域中managed heap中的一個hash table中。其中Key為string本身,Value為string物件的地址。

 當程式域需要一個string的時候,CLR首先在這個Hashtable根據這個string的hash code試著找對應的Item。如果成功找到,則直接把對應的引用返回,否則就在SystemDomain對應的managed heap中建立該 string,並加入到hash table中,並把引用返回。所以說字串的生命週期是基於整個程式的,也是跨AppDomain。

Monitor的用法

簡單介紹下Wait,Pulse,PulseAll的用法,已加註釋。

 static string str = "mushroom";
        static void Main(string[] args)
        {
            new Thread(() =>
            {
                bool isGetLock = false;
                Monitor.Enter(str, ref isGetLock);
                try
                {
                    Console.WriteLine("Thread1第一次獲取鎖");
                    Thread.Sleep(5000);
                    Console.WriteLine("Thread1暫時釋放鎖,並等待其他執行緒釋放通知訊號。");
                    Monitor.Wait(str); 
                    Console.WriteLine("Thread1接到通知,第二次獲取鎖。");
                    Thread.Sleep(1000);
                } 
                finally
                {
                    if (isGetLock)
                    {
                        Monitor.Exit(str);
                        Console.WriteLine("Thread1釋放鎖");
                    }
                }
            }).Start();
            Thread.Sleep(1000);
            new Thread(() =>
            {
                bool isGetLock = false;
                Monitor.Enter(str, ref isGetLock); //一直等待中,直到其他釋放。
                try
                {
                    Console.WriteLine("Thread2獲得鎖");
                    Thread.Sleep(5000);
                    Monitor.Pulse(str); //通知佇列裡一個執行緒,改變鎖狀態。  Pulseall 通知所有的
                    Console.WriteLine("Thread2通知其他執行緒,改變狀態。");
                    Thread.Sleep(1000);
                }
                finally
                {
                    if (isGetLock)
                    {
                        Monitor.Exit(str);
                        Console.WriteLine("Thread2釋放鎖");
                    }
                }

            }).Start();
            Console.ReadLine();

Mutex

 lock是不能跨程式鎖的。 mutex作用和lock類似,但是它能跨程式鎖資源(走的是windows核心構造),如例子:

    static bool createNew = false;
        //第一個引數 是否應擁有互斥體的初始所屬權。即createNew true時,mutex預設獲得處理訊號
        //第二個是名字,第三個是否成功。
        public static Mutex mutex = new Mutex(true, "mushroom.mutex", out createNew);

        static void Main(string[] args)
        {
            //======Example 5=====
            if (createNew)  //第一個建立成功,這時候已經拿到鎖了。 無需再WaitOne了。一定要注意。
            {
                try
                {
                    Run();
                }
                finally
                {
                    mutex.ReleaseMutex(); //釋放當前鎖。  
                }
            }
            //WaitOne 函式作用是阻止當前執行緒,直到拿到收到其他例項釋放的處理訊號。
            //第一個引數是等待超時時間,第二個是否退出上下文同步域。
            else if (mutex.WaitOne(10000,false))//
            {
                try
                {
                    Run();
                }
                finally
                {
                    mutex.ReleaseMutex();
                }
            }
            else//如果沒有發現處理訊號
            {
                Console.WriteLine("已經有例項了。");
                Console.ReadLine();
            }
        }
        static void Run()
        {
            Console.WriteLine("例項1");
            Console.ReadLine();
        }

順序啟動A  B例項測試下。A首先拿到鎖,輸出 例項1 。B在等待, 如果10秒內A釋放,B拿到執行Run()。超時後輸出"已經有例項了"。

這裡注意的是第一個拿到處理訊號 的例項,已經拿到鎖了。不需要再WaitOne。  否則報異常。  

Semaphore

 即訊號量,我們可以把它理解為升級版的mutex。mutex對一個資源進行鎖,semaphore則是對多個資源進行加鎖。

semaphore是由windows核心維持一個int32變數的執行緒計數器,執行緒每呼叫一次、計數器減一、釋放後對應加一, 超出的執行緒則排隊等候。

走的是核心構造,所以semaphore也是可以跨程式的。

 static void Main(string[] args)
        {
            Console.WriteLine("準備處理佇列");

            bool createNew = false;

            SemaphoreSecurity ss = new SemaphoreSecurity(); //訊號量許可權控制
            Semaphore semaphore = new Semaphore(2, 2, "mushroom.Semaphore", out createNew,null);
            for (int i = 1; i <= 5; i++)
            {
                new Thread((arg) =>
                {
                    semaphore.WaitOne();
                    Console.WriteLine(arg + "處理中");
                    Thread.Sleep(10000);
                    semaphore.Release(); //即semaphore.Release(1)
                    //semaphore.Release(5);可以釋放多個,但不能超過最大值。如果最後釋放的總量超過本身總量,也會報錯。 不建議使用

                }).Start(i);
            }
            Console.ReadLine();
        }

總結

mutex、Semaphore  需要先把託管程式碼轉成本地使用者模式程式碼、再轉換成本地核心程式碼。  

當釋放後需要重新轉換成託管程式碼,效能會有一定的損耗,所以儘量在需要跨程式的場景再使用。 

參考 http://www.cnblogs.com/artech/archive/2007/06/04/769805.html

相關文章