在多線程(線程同步)中,我們將學習多線程中操作共享資源的技術,學習到的知識點如下所示:
一、執行基本的原子操作
在這一小節中,我們將學習如何在沒有阻塞線程(blocking threads)發生的情況下,在一個對象上執行基本的原子操作並能阻止競爭條件(race condition)的發生。操作步驟如下所示:
1、使用Visual Studio 2015創建一個新的控制台應用程序。
2、雙擊打開“Program.cs”文件,編寫代碼如下所示:
1 using System; 2 using System.Threading; 3 using static System.Console; 4 5 namespace Recipe01 6 { 7 abstract class CounterBase 8 { 9 public abstract void Increment(); 10 11 public abstract void Decrement(); 12 } 13 14 class Counter : CounterBase 15 { 16 private int count; 17 18 public int Count => count; 19 20 public override void Increment() 21 { 22 count++; 23 } 24 25 public override void Decrement() 26 { 27 count--; 28 } 29 } 30 31 class CounterNoLock : CounterBase 32 { 33 private int count; 34 35 public int Count => count; 36 37 public override void Increment() 38 { 39 Interlocked.Increment(ref count); 40 } 41 42 public override void Decrement() 43 { 44 Interlocked.Decrement(ref count); 45 } 46 } 47 48 class Program 49 { 50 static void TestCounter(CounterBase c) 51 { 52 for (int i = 0; i < 100000; i++) 53 { 54 c.Increment(); 55 c.Decrement(); 56 } 57 } 58 59 static void Main(string[] args) 60 { 61 WriteLine("Incorrect counter"); 62 63 var c1 = new Counter(); 64 65 var t1 = new Thread(() => TestCounter(c1)); 66 var t2 = new Thread(() => TestCounter(c1)); 67 var t3 = new Thread(() => TestCounter(c1)); 68 t1.Start(); 69 t2.Start(); 70 t3.Start(); 71 t1.Join(); 72 t2.Join(); 73 t3.Join(); 74 75 WriteLine($"Total count: {c1.Count}"); 76 WriteLine("--------------------------"); 77 78 WriteLine("Correct counter"); 79 80 var c2 = new CounterNoLock(); 81 82 t1 = new Thread(() => TestCounter(c2)); 83 t2 = new Thread(() => TestCounter(c2)); 84 t3 = new Thread(() => TestCounter(c2)); 85 t1.Start(); 86 t2.Start(); 87 t3.Start(); 88 t1.Join(); 89 t2.Join(); 90 t3.Join(); 91 92 WriteLine($"Total count: {c2.Count}"); 93 } 94 } 95 }
3、運行該控制台應用程序,運行效果(每次運行效果可能不同)如下圖所示:
在第63行代碼處,我們創建了一個非線程安全的Counter類的一個對象c1,由於它是非線程安全的,因此會發生競爭條件(race condition)。
在第65~67行代碼處,我們創建了三個線程來運行c1對象的“TestCounter”方法,在該方法中,我們按順序對c1對象的count變量執行自增和自減操作。由於c1不是線程安全的,因此在這種情況下,我們得到的counter值是不確定的,我們可以得到0值,但多運行幾次,多數情況下會得到不是0值得錯誤結果。
在多線程(基礎篇)中,我們使用lock關鍵字鎖定對象來解決這個問題,但是使用lock關鍵字會造成其他線程的阻塞。但是,在本示例中我們沒有使用lock關鍵字,而是使用了Interlocked構造,它對於基本的數學操作提供了自增(Increment)、自減(Decrement)以及其他一些方法。
二、使用Mutex構造
在這一小節中,我們將學習如何使用Mutex構造同步兩個單獨的程序,即進程間的同步。具體步驟如下所示:
1、使用Visual Studio 2015創建一個新的控制台應用程序。
2、雙擊打開“Program.cs”文件,編寫代碼如下所示:
1 using System; 2 using System.Threading; 3 using static System.Console; 4 5 namespace Recipe02 6 { 7 class Program 8 { 9 static void Main(string[] args) 10 { 11 const string MutexName = "Multithreading"; 12 13 using (var m = new Mutex(false, MutexName)) 14 { 15 // WaitOne方法的作用是阻止當前線程,直到收到其他實例釋放的處理信號。 16 // 第一個參數是等待超時時間,第二個是否退出上下文同步域。 17 if (!m.WaitOne(TimeSpan.FromSeconds(10), false)) 18 { 19 WriteLine("Second instance is running!"); 20 ReadLine(); 21 } 22 else 23 { 24 WriteLine("Running!"); 25 ReadLine(); 26 // 釋放互斥資源 27 m.ReleaseMutex(); 28 } 29 } 30 31 ReadLine(); 32 } 33 } 34 }
3、編譯代碼,執行兩次該程序,運行效果如下所示:
第一種情況的運行結果:
第二種情況的運行結果:
在第11行代碼處,我們定義了一個mutex(互斥量)的名稱為“Multithreading”,並在第13行代碼處將其傳遞給了Mutex類的構造方法,該構造方法的第一個參數initialOwner我們賦值為false,這允許程序獲得一個已經被創建的mutex。如果沒有任何線程鎖定互斥資源,程序只簡單地顯示“Running”,然後等待按下任何鍵以釋放互斥資源。
如果我們啟動該程序的第二個實例,如果在10秒內我們沒有在第一個實例下按下任何按鈕以釋放互斥資源,那麼在第二個實例中就會顯示“Second instance is running!”,如第一種情況的運行結果所示。如果在10內我們在第一個實例中按下任何鍵以釋放互斥資源,那麼在第二個實例中就會顯示“Running”,如第二種情況的運行結果所示。
三、使用SemaphoreSlim構造
在這一小節中,我們將學習如何在SemaphoreSlim構造的幫助下,限制同時訪問資源的線程數量。具體步驟如下所示:
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 Recipe03 7 { 8 class Program 9 { 10 static SemaphoreSlim semaphore = new SemaphoreSlim(4); 11 12 static void AccessDatabase(string name, int seconds) 13 { 14 WriteLine($"{name} waits to access a database"); 15 semaphore.Wait(); 16 WriteLine($"{name} was granted an access to a database"); 17 Sleep(TimeSpan.FromSeconds(seconds)); 18 WriteLine($"{name} is completed"); 19 semaphore.Release(); 20 } 21 22 static void Main(string[] args) 23 { 24 for(int i = 1; i <= 6; i++) 25 { 26 string threadName = "Thread" + i; 27 int secondsToWait = 2 + 2 * i; 28 var t = new Thread(() => AccessDatabase(threadName, secondsToWait)); 29 t.Start(); 30 } 31 } 32 } 33 }
3、運行該控制台應用程序,運行效果(每次運行效果可能不同)如下圖所示:
在第10行代碼處,我們創建了一個SemaphoreSlim的實例,並對該構造方法傳遞了參數4,該參數指定了可以有多少個線程同時訪問資源。然後,我們啟動了6個不同名字的線程。每個線程都試著獲取對數據庫的訪問,但是,我們限制了最多只有4個線程可以訪問數據庫,因此,當4個線程訪問數據庫後,其他2個線程必須等待,直到其他線程完成其工作後,調用“Release”方法釋放資源之後才能訪問數據庫。