在上一篇多線程(基礎篇2)中,我們主要講述了確定線程的狀態、線程優先級、前台線程和後台線程以及向線程傳遞參數的知識,在這一篇中我們將講述如何使用C#的lock關鍵字鎖定線程、使用Monitor鎖定線程以及線程中的異常處理。
九、使用C#的lock關鍵字鎖定線程
1、使用Visual Studio 2015創建一個新的控制台應用程序。
2、雙擊打開“Program.cs”文件,然後修改為如下代碼:
1 using System; 2 using System.Threading; 3 using static System.Console; 4 5 namespace Recipe09 6 { 7 abstract class CounterBase 8 { 9 public abstract void Increment(); 10 public abstract void Decrement(); 11 } 12 13 class Counter : CounterBase 14 { 15 public int Count { get; private set; } 16 17 public override void Increment() 18 { 19 Count++; 20 } 21 22 public override void Decrement() 23 { 24 Count--; 25 } 26 } 27 28 class CounterWithLock : CounterBase 29 { 30 private readonly object syncRoot = new Object(); 31 32 public int Count { get; private set; } 33 34 public override void Increment() 35 { 36 lock (syncRoot) 37 { 38 Count++; 39 } 40 } 41 42 public override void Decrement() 43 { 44 lock (syncRoot) 45 { 46 Count--; 47 } 48 } 49 } 50 51 class Program 52 { 53 static void TestCounter(CounterBase c) 54 { 55 for (int i = 0; i < 100000; i++) 56 { 57 c.Increment(); 58 c.Decrement(); 59 } 60 } 61 62 static void Main(string[] args) 63 { 64 WriteLine("Incorrect counter"); 65 var c1 = new Counter(); 66 var t1 = new Thread(() => TestCounter(c1)); 67 var t2 = new Thread(() => TestCounter(c1)); 68 var t3 = new Thread(() => TestCounter(c1)); 69 t1.Start(); 70 t2.Start(); 71 t3.Start(); 72 t1.Join(); 73 t2.Join(); 74 t3.Join(); 75 WriteLine($"Total count: {c1.Count}"); 76 77 WriteLine("--------------------------"); 78 79 WriteLine("Correct counter"); 80 var c2 = new CounterWithLock(); 81 t1 = new Thread(() => TestCounter(c2)); 82 t2 = new Thread(() => TestCounter(c2)); 83 t3 = new Thread(() => TestCounter(c2)); 84 t1.Start(); 85 t2.Start(); 86 t3.Start(); 87 t1.Join(); 88 t2.Join(); 89 t3.Join(); 90 WriteLine($"Total count: {c2.Count}"); 91 } 92 } 93 }
3、運行該控制台應用程序,運行效果(每次運行效果可能不同)如下圖所示:
在第65行代碼處,我們創建了Counter類的一個對象,該類定義了一個簡單的counter變量,該變量可以自增1和自減1。然後在第66~68行代碼處,我們創建了三個線程,並利用lambda表達式將Counter對象傳遞給了“TestCounter”方法,這三個線程共享同一個counter變量,並且對這個變量進行自增和自減操作,這將導致結果的不正確。如果我們多次運行這個控制台程序,它將打印出不同的counter值,有可能是0,但大多數情況下不是。
發生這種情況是因為Counter類是非線程安全的。我們假設第一個線程在第57行代碼處執行完畢後,還沒有執行第58行代碼時,第二個線程也執行了第57行代碼,這個時候counter的變量值自增了2次,然後,這兩個線程同時執行了第58行處的代碼,這會造成counter的變量只自減了1次,因此,造成了不正確的結果。
為了確保不發生上述不正確的情況,我們必須保證在某一個線程訪問counter變量時,另外所有的線程必須等待其執行完畢才能繼續訪問,我們可以使用lock關鍵字來完成這個功能。如果我們在某個線程中鎖定一個對象,其他所有線程必須等到該線程解鎖之後才能訪問到這個對象,因此,可以避免上述情況的發生。但是要注意的是,使用這種方式會嚴重影響程序的性能。更好的方式我們將會在仙童同步中講述。
十、使用Monitor鎖定線程
在這一小節中,我們將描述一個多線程編程中的常見的一個問題:死鎖。我們首先創建一個死鎖的示例,然後使用Monitor避免死鎖的發生。
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 Recipe10 7 { 8 class Program 9 { 10 static void LockTooMuch(object lock1, object lock2) 11 { 12 lock (lock1) 13 { 14 Sleep(1000); 15 lock (lock2) 16 { 17 } 18 } 19 } 20 21 static void Main(string[] args) 22 { 23 object lock1 = new object(); 24 object lock2 = new object(); 25 26 new Thread(() => LockTooMuch(lock1, lock2)).Start(); 27 28 lock (lock2) 29 { 30 WriteLine("This will be a deadlock!"); 31 Sleep(1000); 32 lock (lock1) 33 { 34 WriteLine("Acquired a protected resource succesfully"); 35 } 36 } 37 } 38 } 39 }
3、運行該控制台應用程序,運行效果如下圖所示:
在上述結果中我們可以看到程序發生了死鎖,程序一直結束不了。
在第10~19行代碼處,我們定義了一個名為“LockTooMuch”的方法,在該方法中我們鎖定了第一個對象lock1,等待1秒鐘後,希望鎖定第二個對象lock2。
在第26行代碼處,我們創建了一個新的線程來執行“LockTooMuch”方法,然後立即執行第28行代碼。
在第28~32行代碼處,我們在主線程中鎖定了對象lock2,然後等待1秒鐘後,希望鎖定第一個對象lock1。
在創建的新線程中我們鎖定了對象lock1,等待1秒鐘,希望鎖定對象lock2,而這個時候對象lock2已經被主線程鎖定,所以新建線程會等待對象lock2被主線程解鎖。然而,在主線程中,我們鎖定了對象lock2,等待1秒鐘,希望鎖定對象lock1,而這個時候對象lock1已經被創建的線程鎖定,所以主線程會等待對象lock1被創建的線程解鎖。當發生這種情況的時候,死鎖就發生了,所以我們的控制台應用程序目前無法正常結束。
4、要避免死鎖的發生,我們可以使用“Monitor.TryEnter”方法來替換lock關鍵字,“Monitor.TryEnter”方法在請求不到資源時不會阻塞等待,可以設置超時時間,獲取不到直接返回false。修改代碼如下所示:
1 using System; 2 using System.Threading; 3 using static System.Console; 4 using static System.Threading.Thread; 5 6 namespace Recipe10 7 { 8 class Program 9 { 10 static void LockTooMuch(object lock1, object lock2) 11 { 12 lock (lock1) 13 { 14 Sleep(1000); 15 lock (lock2) 16 { 17 } 18 } 19 } 20 21 static void Main(string[] args) 22 { 23 object lock1 = new object(); 24 object lock2 = new object(); 25 26 new Thread(() => LockTooMuch(lock1, lock2)).Start(); 27 28 lock (lock2) 29 { 30 WriteLine("This will be a deadlock!"); 31 Sleep(1000); 32 //lock (lock1) 33 //{ 34 // WriteLine("Acquired a protected resource succesfully"); 35 //} 36 if (Monitor.TryEnter(lock1, TimeSpan.FromSeconds(5))) 37 { 38 WriteLine("Acquired a protected resource succesfully"); 39 } 40 else 41 { 42 WriteLine("Timeout acquiring a resource!"); 43 } 44 } 45 } 46 } 47 }
5、運行該控制台應用程序,運行效果如下圖所示:
此時,我們的控制台應用程序就避免了死鎖的發生。
十一、處理異常
在這一小節中,我們講述如何在線程中正確地處理異常。正確地將try/catch塊放置在線程內部是非常重要的,因為在線程外部捕獲線程內部的異常通常是不可能的。
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 Recipe11 7 { 8 class Program 9 { 10 static void BadFaultyThread() 11 { 12 WriteLine("Starting a faulty thread..."); 13 Sleep(TimeSpan.FromSeconds(2)); 14 throw new Exception("Boom!"); 15 } 16 17 static void FaultyThread() 18 { 19 try 20 { 21 WriteLine("Starting a faulty thread..."); 22 Sleep(TimeSpan.FromSeconds(1)); 23 throw new Exception("Boom!"); 24 } 25 catch(Exception ex) 26 { 27 WriteLine($"Exception handled: {ex.Message}"); 28 } 29 } 30 31 static void Main(string[] args) 32 { 33 var t = new Thread(FaultyThread); 34 t.Start(); 35 t.Join(); 36 37 try 38 { 39 t = new Thread(BadFaultyThread); 40 t.Start(); 41 } 42 catch (Exception ex) 43 { 44 WriteLine(ex.Message); 45 WriteLine("We won't get here!"); 46 } 47 } 48 } 49 }
3、運行該控制台應用程序,運行效果如下圖所示:
在第10~15行代碼處,我們定義了一個名為“BadFaultyThread”的方法,在該方法中拋出一個異常,並且沒有使用try/catch塊捕獲該異常。
在第17~29行代碼處,我們定義了一個名為“FaultyThread”的方法,在該方法中也拋出一個異常,但是我們使用了try/catch塊捕獲了該異常。
在第33~35行代碼處,我們創建了一個線程,在該線程中執行了“FaultyThread”方法,我們可以看到在這個新創建的線程中,我們正確地捕獲了在“FaultyThread”方法中拋出的異常。
在第37~46行代碼處,我們又新創建了一個線程,在該線程中執行了“BadFaultyThread”方法,並且在主線程中使用try/catch塊來捕獲在新創建的線程中拋出的異常,不幸的的是我們在主線程中無法捕獲在新線程中拋出的異常。
由此可以看到,在一個線程中捕獲另一個線程中的異常通常是不可行的。
至此,多線程(基礎篇)我們就講述到這兒,之後我們將講述線程同步相關的知識,敬請期待!
源碼下載