一、線程同步中的一些概念
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(); } }
輸出如下:
總結:寫入鎖與任何鎖都不兼容,讀取鎖與讀取鎖可以兼容。