1. 程式人生 > >鎖機制與原子操作

鎖機制與原子操作

一、執行緒同步中的一些概念

  1.1臨界區(共享區)的概念

  在多執行緒的環境中,可能需要共同使用一些公共資源,這些資源可能是變數,方法邏輯段等等,這些被多個執行緒共用的區域統稱為臨界區(共享區),臨界區的資源不是很安全,因為執行緒的狀態是不定的,所以可能帶來的結果是臨界區的資源遭到其他執行緒的破壞,我們必須採取策略或者措施讓共享區資料在多執行緒的環境下保持完成性不讓其受到多執行緒訪問的破壞。

  1.2基元使用者模式

  基元使用者模式是指使用cpu的特殊指令來排程執行緒,所以這種協調排程執行緒是在硬體中進行的所以得出了它第一些優點:

  • 速度特別快;
  • 執行緒阻塞時間特別短;

  但是由於該模式中的執行緒可能被系統搶佔,導致該模式中的執行緒為了獲取某個資源,而浪費許多cpu時間,同時如果一直處於等待的話會導致”活鎖”,也就是既浪費了記憶體,又浪費了cpu時間,這比下文中的死鎖更可怕,那麼如何利用強大的cpu時間做更多的事呢?那就引出了下面的一個模式

   1.3基元核心模式

  該模式和使用者模式不同,它是windows系統自身提供的,使用了作業系統中核心函式,所以它能夠阻塞執行緒提高了cpu的利用率,同時也帶來了一個很可怕的bug,死鎖,可能執行緒會一直阻塞導致程式的奔潰,常用的核心模式的技術例如Monitor,Mutex,等等會在下一章節介紹。本章將詳細討論鎖的概念,使用方法和注意事項

   1.4原子性操作

  如果一個語句執行一個單獨不可分割的指令,那麼它是原子的。嚴格的原子操作排除了任何搶佔的可能性,更方便的理解是這個值永遠是最新的,在c#中原子操作如下圖所示:其實要符合原子操作必須滿足以下條件c#中如果是32位cpu的話,為一個少於等於32位欄位賦值是原子操作,其他(自增,讀,寫操作)的則不是。對於64位cpu而言,操作32或64位的欄位賦值都屬於原子操作其他讀寫操作都不能屬於原子操作相信大家能夠理解原子的特點,所以在使用原子操作時也需要注意當前作業系統是32位或是64位cpu或者兩者皆要考慮。

  1.5非阻止同步

  非阻止同步:不阻止其他執行緒的情況下實現同步。就是利用原子性操作實現執行緒間的同步,不刻意阻塞執行緒,減少相應執行緒的開銷,interlocked類便是c#中非阻止同步的理念所產生的執行緒同步技術。

  1.6阻止同步

  阻止同步:阻止其他執行緒,同一時間只允許單個執行緒訪問臨界資源。其實阻止同步也是基元核心模式的特點之一。

  例如c# 中的鎖機制,及mutex,monitor等都屬於阻止同步,他們的根本目的是,以互斥的效果讓同一時間只有一個執行緒能夠訪問共享區,其他執行緒必須阻止等待,直到該執行緒離開共享區後,才讓其他一個執行緒訪問共享區,阻止同步缺點也是容易產生死鎖,但是阻止同步提高了cpu時間的利用率。

二、為何需要同步

  當多個執行緒同時訪問某個資源,可能造成意想不到的結果。如多個執行緒同時訪問靜態資源。

複製程式碼

    class Program
    {
        static void Main(string[] args)
        {
            //初始化10個執行緒1去訪問num
            for (int i = 0; i < 10; i++)
            {
                ThreadPool.QueueUserWorkItem(new WaitCallback(Run));
            }
            Console.ReadKey();
        }

        static int num = 0;

        static void Run(object state)
        {
            Console.WriteLine("當前數字:{0}", ++num);
        }
    }

複製程式碼

  輸出如下:

  

  我們看到,num++按照邏輯,應該是1,2,3,4,5,6,7,8,9,10。這就是多個執行緒去訪問,順序亂套了。這時候就需要同步了。

三、原子操作同步原理

  Thread類中的VolatileRead和VolatileWrite方法:

  • VolatileWrite:當執行緒在共享區(臨界區)傳遞資訊時,通過此方法來原子性的寫入最後一個值;
  • VolatileRead:當執行緒在共享區(臨界區)傳遞資訊時,通過此方法來原子性的讀取第一個值;

複製程式碼

    class Program
    {
        static Int32 count;//計數值,用於執行緒同步 (注意原子性,所以本例中使用int32)
        static Int32 value;//實際運算值,用於顯示計算結果

        static void Main(string[] args)
        {
            //讀執行緒
            Thread thread2 = new Thread(new ThreadStart(Read));
            thread2.Start();

            //寫執行緒
            for (int i = 0; i < 10; i++)
            {
                Thread.Sleep(20);
                Thread thread = new Thread(new ThreadStart(Write));
                thread.Start();
            }
            Console.ReadKey();
        }

        /// <summary>
        /// 實際運算寫操作
        /// </summary>
        private static void Write()
        {
            Int32 temp = 0;
            for (int i = 0; i < 10; i++)
            {
                temp += 1;
            }
            //真正寫入
            value += temp;
            Thread.VolatileWrite(ref count, 1);
        }

        /// <summary>
        ///  死迴圈監控讀資訊
        /// </summary>
        private static void Read()
        {
            while (true)
            {
                //死迴圈監聽寫操作線執行完畢後立刻顯示操作結果
                if (Thread.VolatileRead(ref count) > 0)
                {
                    Console.WriteLine("累計計數:{1}", Thread.CurrentThread.ManagedThreadId, value);
                    count = 0;
                }
            }
        }
    }

複製程式碼

  輸出如下:

  

三、Volatile關鍵字

  Volatile關鍵字的本質含義是告訴編譯器,宣告為Volatile關鍵字的變數或欄位都是提供給多個執行緒使用的。Volatile無法宣告為區域性變數。作為原子性的操作,Volatile關鍵字具有原子特性,所以執行緒間無法對其佔有,它的值永遠是最新的。

  Volatile支援的型別:

  • 引用型別;
  • 指標型別(在不安全的上下文中);
  • 型別,如 sbyte、byte、short、ushort、int、uint、char、float 和 bool;
  • 具有以下基型別之一的列舉型別:byte、sbyte、short、ushort、int 或 uint;
  • 已知為引用型別的泛型型別引數;
  • IntPtr 和 UIntPtr;

複製程式碼

class Program
    {
        static volatile Int32 count;//計數值,用於執行緒同步 (注意原子性,所以本例中使用int32)
        static Int32 value;//實際運算值,用於顯示計算結果
        static void Main(string[] args)
        {
            //開闢一個執行緒專門負責讀value的值,這樣就能看見一個計算的過程
            Thread thread2 = new Thread(new ThreadStart(Read));
            thread2.Start();
            //開闢10個執行緒來負責計算,每個執行緒負責1000萬條資料
            for (int i = 0; i < 10; i++)
            {
                Thread.Sleep(20);
                Thread thread = new Thread(new ThreadStart(Write));
                thread.Start();
            }
            Console.ReadKey();
        }

        /// <summary>
        /// 實際運算寫操作
        /// </summary>
        private static void Write()
        {
            Int32 temp = 0;
            for (int i = 0; i < 10; i++)
            {
                temp += 1;
            }
            value += temp;
            //告訴監聽程式,我改變了,讀取最新吧!
            count = 1;
        }

        /// <summary>
        ///  死迴圈監聽
        /// </summary>
        private static void Read()
        {
            while (true)
            {
                if (count == 1)
                {
                    Console.WriteLine("累計計數:{1}", Thread.CurrentThread.ManagedThreadId, value);
                    count = 0;
                }
            }
        }
    }

複製程式碼

  輸出:

  

四、lock關鍵字

  lock的作用在於同一時間確保一個物件只允許一個執行緒訪問。

  lock的語法如下:

複製程式碼

   static object obj = new object();
   lock (obj)
   {
     //語句塊
   }

複製程式碼

  我們使用lock來改寫上面的示例:

複製程式碼

    class Program
    {
        static void Main(string[] args)
        {
            //初始化10個執行緒1去訪問num
            for (int i = 0; i < 10; i++)
            {
                ThreadPool.QueueUserWorkItem(new WaitCallback(Run));
            }
            Console.ReadKey();
        }

        static int num = 0;

        static object obj = newobject();

        static void Run(object state)
        {
            lock (obj)
        {
                Console.WriteLine("當前數字:{0}", ++num);
            }
        }
    }

複製程式碼

  輸出如下:

  

五、Monitor.Enter與Monitor.Exit

  Monitor.Enter和Monitor.Exit這個東西跟lock的作用一樣。事實上。lock就是Monitor.Enter和Monitor.Exit的包裝。

  下面用Monitor.Enter與Monitor.Exit來實現相同的程式碼:

複製程式碼

    class Program
    {
        static void Main(string[] args)
        {
            //初始化10個執行緒1去訪問num
            for (int i = 0; i < 10; i++)
            {
                ThreadPool.QueueUserWorkItem(new WaitCallback(Run));
            }
            Console.ReadKey();
        }

        static int num = 0;

        static object obj = new object();

        static void Run(object state)
        {
            //獲取排他鎖
        Monitor.Enter(obj);

            Console.WriteLine("當前數字:{0}", ++num);

            //釋放排它鎖
        Monitor.Exit(obj);
        }
    }

複製程式碼

六、Monitor.Wait與Monitor.Pulse

  Wait() 和 Pulse() 機制用於執行緒間互動:

  • Wait() 釋放鎖定資源,進入等待狀態直到被喚醒;
  • Pulse() 和 PulseAll() 方法用來通知Wait()的執行緒醒來;

複製程式碼

    class Program
    {
        static void Main(string[] args)
        {
            Thread t1 = new Thread(Run1);
            Thread t2 = new Thread(Run2);

            t1.Start();
            t1.Name = "劉備";

            t2.Start();
            t2.Name = "關羽";

            Console.ReadKey();
        }

        static object obj = new object();

        static void Run1(object state)
        {
            Monitor.Enter(obj);

            Console.WriteLine(Thread.CurrentThread.Name + ":二弟,你上哪去了?");

            Monitor.Wait(obj);      //暫時釋放鎖,讓關羽執行緒進入

            Console.WriteLine(Thread.CurrentThread.Name + ":你混蛋!");
                
            Monitor.Pulse(obj);     //喚醒關羽執行緒 

            Monitor.Exit(obj);
        }

        static void Run2(object state)
        {

            Monitor.Enter(obj);

            Console.WriteLine(Thread.CurrentThread.Name + ":老子跟曹操了!");

            Monitor.Pulse(obj);     //喚醒劉備執行緒
            Monitor.Wait(obj);     //暫停本執行緒
            

            Console.WriteLine(Thread.CurrentThread.Name + ":投降吧,曹孟德當世英雄,豎子不足與謀!!");

            Monitor.Exit(obj);
        }
    }

複製程式碼

  輸出如下:

  

七、讀寫鎖ReadWriterLock

  寫入序列,讀取並行;

  如果程式中大部分都是讀取資料的,那麼由於讀並不影響資料,ReadWriterLock類能夠實現”寫入序列“,”讀取並行“。

  常用方法如下:

  • AcquireWriterLock: 獲取寫入鎖; ReleaseWriterLock:釋放寫入鎖。
  • AcquireReaderLock: 獲取讀鎖; ReleaseReaderLock:釋放讀鎖。
  • UpgradeToWriterLock:將讀鎖轉為寫鎖;DowngradeFromWriterLock:將寫鎖還原為讀鎖。

複製程式碼

   class Program
    {
        static List<string> ListStr = new List<string>();
        static ReaderWriterLock rw = new System.Threading.ReaderWriterLock();

        static void Main(string[] args)
        {
            Thread t1 = new Thread(Run1);
            Thread t2 = new Thread(Run2);

            t1.Start();
            t1.Name = "劉備";

            t2.Start();
            t2.Name = "關羽";

            Console.ReadKey();
        }

        static object obj = new object();

        static void Run1(object state)
        {
            //獲取寫鎖2秒
            rw.AcquireWriterLock(2000);
            Console.WriteLine(Thread.CurrentThread.Name + "正在寫入!");
            ListStr.Add("曹操混蛋");
            ListStr.Add("孫權王八蛋");
            Thread.Sleep(1200);
            ListStr.Add("周瑜個臭小子");
            rw.ReleaseWriterLock();
            
        }

        //此方法異常,超時,因為寫入時不允許讀(那麼不用測也能猜到更加不允許寫咯)
        static void Run2(object state)
        {
            //獲取讀鎖1秒
            rw.AcquireReaderLock(1000);
            Console.WriteLine(Thread.CurrentThread.Name + "正在讀取!");
            foreach (string str in ListStr)
            {
                Console.WriteLine(str);
            }
            rw.ReleaseReaderLock();
        }
    }

複製程式碼

  異常如下:

  

  下面是讀取並行的例子:

複製程式碼

    class Program
    {
        static List<string> ListStr = new List<string>();
        static ReaderWriterLock rw = new System.Threading.ReaderWriterLock();

        static void Main(string[] args)
        {
            ListStr.Add("貂蟬");
            ListStr.Add("西施");
            ListStr.Add("王昭君");
            Thread t1 = new Thread(Run1);
            Thread t2 = new Thread(Run2);

            t1.Start();
            t1.Name = "劉備";

            t2.Start();
            t2.Name = "關羽";

            Console.ReadKey();
        }

        static object obj = new object();

        static void Run1(object state)
        {
            //獲取寫鎖2秒
            rw.AcquireReaderLock(2000);
            Console.WriteLine(Thread.CurrentThread.Name + "正在讀取!");
            foreach (string str in ListStr)
            {
                Console.WriteLine(Thread.CurrentThread.Name + "在讀:" + str);
            }
            rw.ReleaseReaderLock();
            
        }

        //此方法異常,超時,因為寫入時不允許讀(那麼不用測也能猜到更加不允許寫咯)
        static void Run2(object state)
        {
            //獲取讀鎖1秒
            rw.AcquireReaderLock(1000);
            Console.WriteLine(Thread.CurrentThread.Name + "正在讀取!");
            foreach (string str in ListStr)
            {
                Console.WriteLine(Thread.CurrentThread.Name + "在讀:" + str);
            }
            rw.ReleaseReaderLock();
        }
    }

 

轉自:https://www.cnblogs.com/kissdodog/archive/2013/04/07/3003822.html