在上一篇C#多線程之線程同步2中,我們主要學習了AutoResetEvent構造、ManualResetEventSlim構造和CountdownEvent構造,在這一篇中,我們將學習Barrier構造、ReaderWriterLockSlim構造和SpinWait構造。
七、使用Barrier構造
在這一小節中,我們將學習一個比較有意思的同步構造:Barrier。Barrier構造可以幫助我們控制多個等待線程達到指定數量後,才發送通知信號,然後所有等待線程才能繼續執行,並且在每次等待線程達到指定數量後,還能執行一個回調方法。具體步驟如下所示:
1、使用Visual Studio 2015創建一個新的控制台應用程序。
2、雙擊打開“Program.cs”文件,編寫代碼如下所示:
1 using System; 2 using System.Threading; 3 using static System.Console; 4 using static System.Threading.Thread; 5 6 namespace Recipe07 7 { 8 class Program 9 { 10 static Barrier barrier = new Barrier(2, b => WriteLine($"End of phase {b.CurrentPhaseNumber + 1}")); 11 12 static void PlayMusic(string name, string message, int seconds) 13 { 14 for(int i = 1; i < 3; i++) 15 { 16 WriteLine("----------------------------------------------"); 17 Sleep(TimeSpan.FromSeconds(seconds)); 18 WriteLine($"{name} starts to {message}"); 19 Sleep(TimeSpan.FromSeconds(seconds)); 20 WriteLine($"{name} finishes to {message}"); 21 barrier.SignalAndWait(); 22 } 23 24 } 25 26 static void Main(string[] args) 27 { 28 var t1 = new Thread(() => PlayMusic("the guitarist", "play an amazing solo", 5)); 29 var t2 = new Thread(() => PlayMusic("the singer", "sing his song", 2)); 30 31 t1.Start(); 32 t2.Start(); 33 } 34 } 35 }
3、運行該控制台應用程序,運行效果如下圖所示:
在第10行代碼處,我們創建了一個Barrier的實例barrier,並給其構造方法的“participantCount”參數賦值為2,表示barrier參與線程的數量為2,也就是說要有2個線程達到阻塞後,barrier才發送通知信號,其阻塞線程才能繼續執行。第二個參數“postPhaseAction”是一個Action類型的委托,表示當阻塞線程達到規定數量後要執行的回調方法。
在第28~29行代碼處,我們創建了2個線程t1和t2,用於執行“PlayMusic”方法。t2線程首先執行到第21行代碼處,在這一行代碼中,我們在線程t2中調用了barrier的“SignalAndWait”方法,等待參與數量的線程達到構造方法指定的數量2時,才能繼續執行,因為,在t2線程調用該方法時,只有一個線程t2被阻塞,沒有達到規定數量2,所以,t2線程不能繼續執行。當t1線程執行到第21行代碼處時,也調用了barrier的“SignalAndWait”方法,這個時候等待線程的數量達到規定的數量2,所以t1和t2線程都能繼續執行,並且在barrier的構造方法的第二個參數指定的回調方法也被執行。
當兩個線程執行“PlayMusic”方法的第二次循環時,過程與第一次一樣,不在描述。
八、使用ReaderWriterLockSlim構造
在這一小節中,我們將學習如何使用ReaderWriterLockSlim構造來線程安全地使用多線程讀寫集合中的數據。具體步驟如下所示:
1、使用Visual Studio 2015創建一個新的控制台應用程序。
2、雙擊打開“Program.cs”文件,編寫代碼如下所示:
1 using System; 2 using System.Collections.Generic; 3 using System.Threading; 4 using static System.Console; 5 using static System.Threading.Thread; 6 7 namespace Recipe08 8 { 9 class Program 10 { 11 // 表示用於管理資源訪問的鎖定狀態,可實現多線程讀取或進行獨占式寫入訪問 12 static ReaderWriterLockSlim rw = new ReaderWriterLockSlim(); 13 static Dictionary<int, int> items = new Dictionary<int, int>(); 14 15 static void Read() 16 { 17 WriteLine("Reading contents of a dictionary"); 18 while (true) 19 { 20 try 21 { 22 // 嘗試進入讀取模式鎖定狀態 23 rw.EnterReadLock(); 24 foreach(var key in items.Keys) 25 { 26 Sleep(TimeSpan.FromSeconds(0.1)); 27 } 28 } 29 finally 30 { 31 // 減少讀取模式的遞歸計數,並在生成的計數為 0(零)時退出讀取模式 32 rw.ExitReadLock(); 33 } 34 } 35 } 36 37 static void Write(string threadName) 38 { 39 while (true) 40 { 41 try 42 { 43 int newKey = new Random().Next(250); 44 // 嘗試進入可升級模式鎖定狀態 45 rw.EnterUpgradeableReadLock(); 46 if (!items.ContainsKey(newKey)) 47 { 48 try 49 { 50 // 嘗試進入寫入模式鎖定狀態 51 rw.EnterWriteLock(); 52 items[newKey] = 1; 53 WriteLine($"New key {newKey} is added to a dictionary by a {threadName}"); 54 } 55 finally 56 { 57 // 減少寫入模式的遞歸計數,並在生成的計數為 0(零)時退出寫入模式 58 rw.ExitWriteLock(); 59 } 60 } 61 Sleep(TimeSpan.FromSeconds(0.1)); 62 } 63 finally 64 { 65 // 減少可升級模式的遞歸計數,並在生成的計數為 0(零)時退出可升級模式 66 rw.ExitUpgradeableReadLock(); 67 } 68 } 69 } 70 71 static void Main(string[] args) 72 { 73 new Thread(Read) { IsBackground = true }.Start(); 74 new Thread(Read) { IsBackground = true }.Start(); 75 new Thread(Read) { IsBackground = true }.Start(); 76 77 new Thread(() => Write("Thread 1")) { IsBackground = true }.Start(); 78 new Thread(() => Write("Thread 2")) { IsBackground = true }.Start(); 79 80 Sleep(TimeSpan.FromSeconds(20)); 81 } 82 } 83 }
3、運行該控制台應用程序,運行效果(每次運行效果可能不同)如下圖所示:
在第73~75行代碼處,我們創建了3個後台線程來讀取集合中的數據。在第77~78行代碼處,我們創建了2個後台線程向集合中寫入數據。為了線程安全地對集合進行操作,我們使用為此場景專門設計的ReaderWriterLockSlim構造。該構造有兩種類型的鎖:讀取模式鎖和寫入模式鎖。讀取模式鎖允許多線程讀取數據,寫入模式鎖阻塞其他線程的每一個操作直到寫入模式鎖被釋放為止。
有一個非常有趣的場景,當我們想獲得一個讀取模式鎖從集合中讀取一些數據,並根據這些數據獲得一個寫入模式鎖以更新集合時,如果我們立即就獲得鎖定模式鎖的話不僅消耗的時間多,而且還不允許我們讀取數據,因為當我們獲得一個寫入模式鎖的時候,集合就被鎖定了。為了盡量減少這種時間的浪費,我們可以使用“EnterUpgradeableReadLock”方法獲得讀取模式鎖來讀取數據,如果讀取完畢數據後,我們發現需要更新底層集合,那麼我們可以使用“EnterWriteLock”升級我們的鎖,然後快速執行寫入操作並使用“ExitWriteLock”釋放寫入模式鎖,最後使用“ExitUpgradeableReadLock”釋放可升級模式鎖。
在上述代碼中,我們獲得一個隨機數,然後獲得一個讀取模式鎖,並檢查該隨機數是否已在集合中存在,如果不存在,我們升級該讀取模式鎖為寫入模式鎖,然後向集合中添加一個新的key。使用try/finally塊是一個比較好的方式,它可以保證我們總能釋放鎖獲得的鎖。
九、使用SpinWait構造
在這一小節中,我們將學習如何在不涉及kernel-mode構造的情況下等待一個線程的執行。另外還將介紹SpinWait構造,該構造是一種混合同步構造,主要用於設計在用戶模式中等待一段時間後,然後將其切換到內核模式,以節省CUP時間。具體步驟如下所示:
1、使用Visual Studio 2015創建一個新的控制台應用程序。
2、雙擊打開“Program.cs”文件,編寫代碼如下所示:
1 using System; 2 using System.Threading; 3 using static System.Console; 4 using static System.Threading.Thread; 5 6 namespace Recipe09 7 { 8 class Program 9 { 10 static volatile bool isCompleted = false; 11 12 static void UserModeWait() 13 { 14 while (!isCompleted) 15 { 16 Write("."); 17 } 18 WriteLine(); 19 WriteLine("Waiting is complete"); 20 } 21 22 static void HybridSpinWait() 23 { 24 // 提供對基於自旋的等待的支持 25 var w = new SpinWait(); 26 while (!isCompleted) 27 { 28 // 執行單一自旋 29 w.SpinOnce(); 30 // 獲取對 System.Threading.SpinWait.SpinOnce 的下一次調用是否將產生處理器,同時觸發強制上下文切換 31 WriteLine(w.NextSpinWillYield); 32 } 33 WriteLine("Waiting is complete"); 34 } 35 36 static void Main(string[] args) 37 { 38 var t1 = new Thread(UserModeWait); 39 var t2 = new Thread(HybridSpinWait); 40 41 WriteLine("Running user mode waiting"); 42 t1.Start(); 43 Sleep(20); 44 isCompleted = true; 45 Sleep(TimeSpan.FromSeconds(1)); 46 isCompleted = false; 47 WriteLine("Running hybrid SpinWait construct waiting"); 48 t2.Start(); 49 Sleep(5); 50 isCompleted = true; 51 } 52 } 53 }
3、運行該控制台應用程序,運行效果(每次運行效果可能不同)如下圖所示:
在上述程序中,我們創建了一個線程執行一個無線循環20毫秒,直到在主線程中將isCompleted變量設置為true。我們可以將此時間設置為20-30秒,然後打開任務管理器,我們可以看到CPU的使用率比較高。
我們使用volatile關鍵字聲明了一個名為“isCompleted”的靜態字段。volatile 關鍵字指示一個字段可以由多個同時執行的線程修改。聲明為 volatile 的字段不受編譯器優化(假定由單個線程訪問)的限制。這樣可以確保該字段在任何時間呈現的都是最新的值。
然後,我們使用SpinWait版本,在第29行代碼處,我們調用了SpinWait的“SpinOnce”方法,執行一次自旋。當SpinWait自旋達到一定次數後,如果有必要當前線程會讓出底層的時間片並觸發上下文切換。在這個版本中,如果我們將第49行代碼的等待時間修改為20~30秒,然後打開任務管理器,可以發現CPU使用率是比較低的。
至此,關於線程同步的知識就學習到這兒!
源碼下載