C#多线程编程系列(二)- 线程基础
目錄
- C#多線程編程系列(二)- 線程基礎(chǔ)
- 1.1 簡介
- 1.2 創(chuàng)建線程
- 1.3 暫停線程
- 1.4 線程等待
- 1.5 終止線程
- 1.6 檢測線程狀態(tài)
- 1.7 線程優(yōu)先級
- 1.8 前臺線程和后臺線程
- 1.9 向線程傳遞參數(shù)
- 1.10 C# Lock關(guān)鍵字的使用
- 1.11 使用Monitor類鎖定資源
- 1.12 多線程中處理異常
- 參考書籍
- 筆者水平有限,如果錯誤歡迎各位批評指正!
C#多線程編程系列(二)- 線程基礎(chǔ)
1.1 簡介#
線程基礎(chǔ)主要包括線程創(chuàng)建、掛起、等待和終止線程。關(guān)于更多的線程的底層實現(xiàn),CPU時間片輪轉(zhuǎn)等等的知識,可以參考《深入理解計算機系統(tǒng)》一書中關(guān)于進程和線程的章節(jié),本文不過多贅述。
1.2 創(chuàng)建線程#
在C#語言中,創(chuàng)建線程是一件非常簡單的事情;它只需要用到?System.Threading命名空間,其中主要使用Thread類來創(chuàng)建線程。
演示代碼如下所示:
Copy
using System; using System.Threading; // 創(chuàng)建線程需要用到的命名空間 namespace Recipe1 { class Program { static void Main(string[] args) { // 1.創(chuàng)建一個線程 PrintNumbers為該線程所需要執(zhí)行的方法 Thread t = new Thread(PrintNumbers); // 2.啟動線程 t.Start(); // 主線程也運行PrintNumbers方法,方便對照 PrintNumbers(); // 暫停一下 Console.ReadKey(); } static void PrintNumbers() { // 使用Thread.CurrentThread.ManagedThreadId 可以獲取當(dāng)前運行線程的唯一標(biāo)識,通過它來區(qū)別線程 Console.WriteLine($"線程:{Thread.CurrentThread.ManagedThreadId} 開始打印..."); for (int i = 0; i < 10; i++) { Console.WriteLine($"線程:{Thread.CurrentThread.ManagedThreadId} 打印:{i}"); } } } }
運行結(jié)果如下圖所示,我們可以通過運行結(jié)果得知上面的代碼創(chuàng)建了一個線程,然后主線程和創(chuàng)建的線程交叉輸出結(jié)果,這說明PrintNumbers方法同時運行在主線程和另外一個線程中。
1.3 暫停線程#
暫停線程這里使用的方式是通過Thread.Sleep方法,如果線程執(zhí)行Thread.Sleep方法,那么操作系統(tǒng)將在指定的時間內(nèi)不為該線程分配任何時間片。如果Sleep時間100ms那么操作系統(tǒng)將至少讓該線程睡眠100ms或者更長時間,所以Thread.Sleep方法不能作為高精度的計時器使用。
演示代碼如下所示:
Copy
using System; using System.Threading; // 創(chuàng)建線程需要用到的命名空間 namespace Recipe2 { class Program { static void Main(string[] args) { // 1.創(chuàng)建一個線程 PrintNumbers為該線程所需要執(zhí)行的方法 Thread t = new Thread(PrintNumbersWithDelay); // 2.啟動線程 t.Start(); // 暫停一下 Console.ReadKey(); } static void PrintNumbersWithDelay() { Console.WriteLine($"線程:{Thread.CurrentThread.ManagedThreadId} 開始打印... 現(xiàn)在時間{DateTime.Now.ToString("HH:mm:ss.ffff")}"); for (int i = 0; i < 10; i++) { //3. 使用Thread.Sleep方法來使當(dāng)前線程睡眠,TimeSpan.FromSeconds(2)表示時間為 2秒 Thread.Sleep(TimeSpan.FromSeconds(2)); Console.WriteLine($"線程:{Thread.CurrentThread.ManagedThreadId} 打印:{i} 現(xiàn)在時間{DateTime.Now.ToString("HH:mm:ss.ffff")}"); } } } }
運行結(jié)果如下圖所示,通過下圖可以確定上面的代碼是有效的,通過Thread.Sleep方法,使線程休眠了2秒左右,但是并不是特別精確的2秒。驗證了上面的說法,它的睡眠是至少讓線程睡眠多長時間,而不是一定多長時間。
1.4 線程等待#
在本章中,線程等待使用的是Join方法,該方法將暫停執(zhí)行當(dāng)前線程,直到所等待的另一個線程終止。在簡單的線程同步中會使用到,但它比較簡單,不作過多介紹。
演示代碼如下所示:
Copy
class Program { static void Main(string[] args) { Console.WriteLine($"-------開始執(zhí)行 現(xiàn)在時間{DateTime.Now.ToString("HH:mm:ss.ffff")}-------"); // 1.創(chuàng)建一個線程 PrintNumbersWithDelay為該線程所需要執(zhí)行的方法 Thread t = new Thread(PrintNumbersWithDelay); // 2.啟動線程 t.Start(); // 3.等待線程結(jié)束 t.Join(); Console.WriteLine($"-------執(zhí)行完畢 現(xiàn)在時間{DateTime.Now.ToString("HH:mm:ss.ffff")}-------"); // 暫停一下 Console.ReadKey(); } static void PrintNumbersWithDelay() { Console.WriteLine($"線程:{Thread.CurrentThread.ManagedThreadId} 開始打印... 現(xiàn)在時間{DateTime.Now.ToString("HH:mm:ss.ffff")}"); for (int i = 0; i < 10; i++) { Thread.Sleep(TimeSpan.FromSeconds(2)); Console.WriteLine($"線程:{Thread.CurrentThread.ManagedThreadId} 打印:{i} 現(xiàn)在時間{DateTime.Now.ToString("HH:mm:ss.ffff")}"); } } }
運行結(jié)果如下圖所示,開始執(zhí)行和執(zhí)行完畢兩條信息由主線程打印;根據(jù)其輸出的順序可見主線程是等待另外的線程結(jié)束后才輸出執(zhí)行完畢這條信息。
1.5 終止線程#
終止線程使用的方法是Abort方法,當(dāng)該方法被執(zhí)行時,將嘗試銷毀該線程。通過引發(fā)ThreadAbortException異常使線程被銷毀。但一般不推薦使用該方法,原因有以下幾點。
基于以上原因不推薦使用Abort方法,在實際項目中一般使用CancellationToken來終止線程。
演示代碼如下所示:
Copy
static void Main(string[] args) { Console.WriteLine($"-------開始執(zhí)行 現(xiàn)在時間{DateTime.Now.ToString("HH:mm:ss.ffff")}-------"); // 1.創(chuàng)建一個線程 PrintNumbersWithDelay為該線程所需要執(zhí)行的方法 Thread t = new Thread(PrintNumbersWithDelay); // 2.啟動線程 t.Start(); // 3.主線程休眠6秒 Thread.Sleep(TimeSpan.FromSeconds(6)); // 4.終止線程 t.Abort(); Console.WriteLine($"-------執(zhí)行完畢 現(xiàn)在時間{DateTime.Now.ToString("HH:mm:ss.ffff")}-------"); // 暫停一下 Console.ReadKey(); } static void PrintNumbersWithDelay() { Console.WriteLine($"線程:{Thread.CurrentThread.ManagedThreadId} 開始打印... 現(xiàn)在時間{DateTime.Now.ToString("HH:mm:ss.ffff")}"); for (int i = 0; i < 10; i++) { Thread.Sleep(TimeSpan.FromSeconds(2)); Console.WriteLine($"線程:{Thread.CurrentThread.ManagedThreadId} 打印:{i} 現(xiàn)在時間{DateTime.Now.ToString("HH:mm:ss.ffff")}"); } }
運行結(jié)果如下圖所示,啟動所創(chuàng)建的線程3后,6秒鐘主線程調(diào)用了Abort方法,線程3沒有繼續(xù)執(zhí)行便結(jié)束了;與預(yù)期的結(jié)果一致。
1.6 檢測線程狀態(tài)#
線程的狀態(tài)可通過訪問ThreadState屬性來檢測,ThreadState是一個枚舉類型,一共有10種狀態(tài),狀態(tài)具體含義如下表所示。
| Aborted | 線程處于?Stopped?狀態(tài)中。 |
| AbortRequested | 已對線程調(diào)用了?Thread.Abort?方法,但線程尚未收到試圖終止它的掛起的?System.Threading.ThreadAbortException。 |
| Background | 線程正作為后臺線程執(zhí)行(相對于前臺線程而言)。此狀態(tài)可以通過設(shè)置?Thread.IsBackground?屬性來控制。 |
| Running | 線程已啟動,它未被阻塞,并且沒有掛起的?ThreadAbortException。 |
| Stopped | 線程已停止。 |
| StopRequested | 正在請求線程停止。這僅用于內(nèi)部。 |
| Suspended | 線程已掛起。 |
| SuspendRequested | 正在請求線程掛起。 |
| Unstarted | 尚未對線程調(diào)用?Thread.Start?方法。 |
| WaitSleepJoin | 由于調(diào)用?Wait、Sleep?或?Join,線程已被阻止。 |
下表列出導(dǎo)致狀態(tài)更改的操作。
| 在公共語言運行庫中創(chuàng)建線程。 | Unstarted |
| 線程調(diào)用?Start | Unstarted |
| 線程開始運行。 | Running |
| 線程調(diào)用?Sleep | WaitSleepJoin |
| 線程對其他對象調(diào)用?Wait。 | WaitSleepJoin |
| 線程對其他線程調(diào)用?Join。 | WaitSleepJoin |
| 另一個線程調(diào)用?Interrupt | Running |
| 另一個線程調(diào)用?Suspend | SuspendRequested |
| 線程響應(yīng)?Suspend?請求。 | Suspended |
| 另一個線程調(diào)用?Resume | Running |
| 另一個線程調(diào)用?Abort | AbortRequested |
| 線程響應(yīng)?Abort?請求。 | Stopped |
| 線程被終止。 | Stopped |
演示代碼如下所示:
Copy
static void Main(string[] args) { Console.WriteLine("開始執(zhí)行..."); Thread t = new Thread(PrintNumbersWithStatus); Thread t2 = new Thread(DoNothing); // 使用ThreadState查看線程狀態(tài) 此時線程未啟動,應(yīng)為Unstarted Console.WriteLine($"Check 1 :{t.ThreadState}"); t2.Start(); t.Start(); // 線程啟動, 狀態(tài)應(yīng)為 Running Console.WriteLine($"Check 2 :{t.ThreadState}"); // 由于PrintNumberWithStatus方法開始執(zhí)行,狀態(tài)為Running // 但是經(jīng)接著會執(zhí)行Thread.Sleep方法 狀態(tài)會轉(zhuǎn)為 WaitSleepJoin for (int i = 1; i < 30; i++) { Console.WriteLine($"Check 3 : {t.ThreadState}"); } // 延時一段時間,方便查看狀態(tài) Thread.Sleep(TimeSpan.FromSeconds(6)); // 終止線程 t.Abort(); Console.WriteLine("t線程被終止"); // 由于該線程是被Abort方法終止 所以狀態(tài)為 Aborted或AbortRequested Console.WriteLine($"Check 4 : {t.ThreadState}"); // 該線程正常執(zhí)行結(jié)束 所以狀態(tài)為Stopped Console.WriteLine($"Check 5 : {t2.ThreadState}"); Console.ReadKey(); } static void DoNothing() { Thread.Sleep(TimeSpan.FromSeconds(2)); } static void PrintNumbersWithStatus() { Console.WriteLine("t線程開始執(zhí)行..."); // 在線程內(nèi)部,可通過Thread.CurrentThread拿到當(dāng)前線程Thread對象 Console.WriteLine($"Check 6 : {Thread.CurrentThread.ThreadState}"); for (int i = 1; i < 10; i++) { Thread.Sleep(TimeSpan.FromSeconds(2)); Console.WriteLine($"t線程輸出 :{i}"); } }
運行結(jié)果如下圖所示,與預(yù)期的結(jié)果一致。
1.7 線程優(yōu)先級#
Windows操作系統(tǒng)為搶占式多線程(Preemptive multithreaded)操作系統(tǒng),是因為線程可在任何時間停止(被槍占)并調(diào)度另一個線程。
Windows操作系統(tǒng)中線程有0(最低) ~ 31(最高)的優(yōu)先級,而優(yōu)先級越高所能占用的CPU時間就越多,確定某個線程所處的優(yōu)先級需要考慮進程優(yōu)先級和相對線程優(yōu)先級兩個優(yōu)先級。
下表總結(jié)了進程的優(yōu)先級和線程的相對優(yōu)先級與優(yōu)先級(0~31)的映射關(guān)系。粗體為相對線程優(yōu)先級,斜體為進程優(yōu)先級。
| Time-Critical | 15 | 15 | 15 | 15 | 15 | 31 |
| Highest | 6 | 8 | 10 | 12 | 15 | 26 |
| Above Normal | 5 | 7 | 9 | 11 | 14 | 25 |
| Normal | 4 | 6 | 8 | 10 | 13 | 24 |
| Below Normal | 3 | 5 | 7 | 9 | 12 | 23 |
| Lowest | 2 | 4 | 6 | 8 | 11 | 22 |
| Idle | 1 | 1 | 1 | 1 | 1 | 16 |
而在C#程序中,可更改線程的相對優(yōu)先級,需要設(shè)置Thread的Priority屬性,可設(shè)置為ThreadPriority枚舉類型的五個值之一:Lowest、BelowNormal、Normal、AboveNormal 或 Highest。CLR為自己保留了Idle和Time-Critical優(yōu)先級,程序中不可設(shè)置。
演示代碼如下所示。
Copy
static void Main(string[] args) { Console.WriteLine($"當(dāng)前線程優(yōu)先級: {Thread.CurrentThread.Priority} \r\n"); // 第一次測試,在所有核心上運行 Console.WriteLine("運行在所有空閑的核心上"); RunThreads(); Thread.Sleep(TimeSpan.FromSeconds(2)); // 第二次測試,在單個核心上運行 Console.WriteLine("\r\n運行在單個核心上"); // 設(shè)置在單個核心上運行 System.Diagnostics.Process.GetCurrentProcess().ProcessorAffinity = new IntPtr(1); RunThreads(); Console.ReadLine(); } static void RunThreads() { var sample = new ThreadSample(); var threadOne = new Thread(sample.CountNumbers); threadOne.Name = "線程一"; var threadTwo = new Thread(sample.CountNumbers); threadTwo.Name = "線程二"; // 設(shè)置優(yōu)先級和啟動線程 threadOne.Priority = ThreadPriority.Highest; threadTwo.Priority = ThreadPriority.Lowest; threadOne.Start(); threadTwo.Start(); // 延時2秒 查看結(jié)果 Thread.Sleep(TimeSpan.FromSeconds(2)); sample.Stop(); } class ThreadSample { private bool _isStopped = false; public void Stop() { _isStopped = true; } public void CountNumbers() { long counter = 0; while (!_isStopped) { counter++; } Console.WriteLine($"{Thread.CurrentThread.Name} 優(yōu)先級為 {Thread.CurrentThread.Priority,11} 計數(shù)為 = {counter,13:N0}"); } }
運行結(jié)果如下圖所示。Highest占用的CPU時間明顯多于Lowest。當(dāng)程序運行在所有核心上時,線程可以在不同核心同時運行,所以Highest和Lowest差距會小一些。
1.8 前臺線程和后臺線程#
在CLR中,線程要么是前臺線程,要么就是后臺線程。當(dāng)一個進程的所有前臺線程停止運行時,CLR將強制終止仍在運行的任何后臺線程,不會拋出異常。
在C#中可通過Thread類中的IsBackground屬性來指定是否為后臺線程。在線程生命周期中,任何時候都可從前臺線程變?yōu)楹笈_線程。線程池中的線程默認為后臺線程。
演示代碼如下所示。
Copy
static void Main(string[] args) { var sampleForeground = new ThreadSample(10); var sampleBackground = new ThreadSample(20); var threadPoolBackground = new ThreadSample(20); // 默認創(chuàng)建為前臺線程 var threadOne = new Thread(sampleForeground.CountNumbers); threadOne.Name = "前臺線程"; var threadTwo = new Thread(sampleBackground.CountNumbers); threadTwo.Name = "后臺線程"; // 設(shè)置IsBackground屬性為 true 表示后臺線程 threadTwo.IsBackground = true; // 線程池內(nèi)的線程默認為 后臺線程 ThreadPool.QueueUserWorkItem((obj) => { Thread.CurrentThread.Name = "線程池線程"; threadPoolBackground.CountNumbers(); }); // 啟動線程 threadOne.Start(); threadTwo.Start(); } class ThreadSample { private readonly int _iterations; public ThreadSample(int iterations) { _iterations = iterations; } public void CountNumbers() { for (int i = 0; i < _iterations; i++) { Thread.Sleep(TimeSpan.FromSeconds(0.5)); Console.WriteLine($"{Thread.CurrentThread.Name} prints {i}"); } } }
運行結(jié)果如下圖所示。當(dāng)前臺線程10次循環(huán)結(jié)束以后,創(chuàng)建的后臺線程和線程池線程都會被CLR強制結(jié)束。
1.9 向線程傳遞參數(shù)#
向線程中傳遞參數(shù)常用的有三種方法,構(gòu)造函數(shù)傳值、Start方法傳值和Lambda表達式傳值,一般常用Start方法來傳值。
演示代碼如下所示,通過三種方式來傳遞參數(shù),告訴線程中的循環(huán)最終需要循環(huán)幾次。
Copy
static void Main(string[] args) { // 第一種方法 通過構(gòu)造函數(shù)傳值 var sample = new ThreadSample(10); var threadOne = new Thread(sample.CountNumbers); threadOne.Name = "ThreadOne"; threadOne.Start(); threadOne.Join(); Console.WriteLine("--------------------------"); // 第二種方法 使用Start方法傳值 // Count方法 接收一個Object類型參數(shù) var threadTwo = new Thread(Count); threadTwo.Name = "ThreadTwo"; // Start方法中傳入的值 會傳遞到 Count方法 Object參數(shù)上 threadTwo.Start(8); threadTwo.Join(); Console.WriteLine("--------------------------"); // 第三種方法 Lambda表達式傳值 // 實際上是構(gòu)建了一個匿名函數(shù) 通過函數(shù)閉包來傳值 var threadThree = new Thread(() => CountNumbers(12)); threadThree.Name = "ThreadThree"; threadThree.Start(); threadThree.Join(); Console.WriteLine("--------------------------"); // Lambda表達式傳值 會共享變量值 int i = 10; var threadFour = new Thread(() => PrintNumber(i)); i = 20; var threadFive = new Thread(() => PrintNumber(i)); threadFour.Start(); threadFive.Start(); } static void Count(object iterations) { CountNumbers((int)iterations); } static void CountNumbers(int iterations) { for (int i = 1; i <= iterations; i++) { Thread.Sleep(TimeSpan.FromSeconds(0.5)); Console.WriteLine($"{Thread.CurrentThread.Name} prints {i}"); } } static void PrintNumber(int number) { Console.WriteLine(number); } class ThreadSample { private readonly int _iterations; public ThreadSample(int iterations) { _iterations = iterations; } public void CountNumbers() { for (int i = 1; i <= _iterations; i++) { Thread.Sleep(TimeSpan.FromSeconds(0.5)); Console.WriteLine($"{Thread.CurrentThread.Name} prints {i}"); } } }
運行結(jié)果如下圖所示,與預(yù)期結(jié)果相符。
1.10 C# Lock關(guān)鍵字的使用#
在多線程的系統(tǒng)中,由于CPU的時間片輪轉(zhuǎn)等線程調(diào)度算法的使用,容易出現(xiàn)線程安全問題。具體可參考《深入理解計算機系統(tǒng)》一書相關(guān)的章節(jié)。
在C#中l(wèi)ock關(guān)鍵字是一個語法糖,它將Monitor封裝,給object加上一個互斥鎖,從而實現(xiàn)代碼的線程安全,Monitor會在下一節(jié)中介紹。
對于lock關(guān)鍵字還是Monitor鎖定的對象,都必須小心選擇,不恰當(dāng)?shù)倪x擇可能會造成嚴重的性能問題甚至發(fā)生死鎖。以下有幾條關(guān)于選擇鎖定對象的建議。
以下演示代碼實現(xiàn)了多線程情況下的計數(shù)功能,一種實現(xiàn)是線程不安全的,會導(dǎo)致結(jié)果與預(yù)期不相符,但也有可能正確。另外一種使用了lock關(guān)鍵字進行線程同步,所以它結(jié)果是一定的。
Copy
static void Main(string[] args) { Console.WriteLine("錯誤的多線程計數(shù)方式"); var c = new Counter(); // 開啟3個線程,使用沒有同步塊的計數(shù)方式對其進行計數(shù) var t1 = new Thread(() => TestCounter(c)); var t2 = new Thread(() => TestCounter(c)); var t3 = new Thread(() => TestCounter(c)); t1.Start(); t2.Start(); t3.Start(); t1.Join(); t2.Join(); t3.Join(); // 因為多線程 線程搶占等原因 其結(jié)果是不一定的 碰巧可能為0 Console.WriteLine($"Total count: {c.Count}"); Console.WriteLine("--------------------------"); Console.WriteLine("正確的多線程計數(shù)方式"); var c1 = new CounterWithLock(); // 開啟3個線程,使用帶有l(wèi)ock同步塊的方式對其進行計數(shù) t1 = new Thread(() => TestCounter(c1)); t2 = new Thread(() => TestCounter(c1)); t3 = new Thread(() => TestCounter(c1)); t1.Start(); t2.Start(); t3.Start(); t1.Join(); t2.Join(); t3.Join(); // 其結(jié)果是一定的 為0 Console.WriteLine($"Total count: {c1.Count}"); Console.ReadLine(); } static void TestCounter(CounterBase c) { for (int i = 0; i < 100000; i++) { c.Increment(); c.Decrement(); } } // 線程不安全的計數(shù) class Counter : CounterBase { public int Count { get; private set; } public override void Increment() { Count++; } public override void Decrement() { Count--; } } // 線程安全的計數(shù) class CounterWithLock : CounterBase { private readonly object _syncRoot = new Object(); public int Count { get; private set; } public override void Increment() { // 使用Lock關(guān)鍵字 鎖定私有變量 lock (_syncRoot) { // 同步塊 Count++; } } public override void Decrement() { lock (_syncRoot) { Count--; } } } abstract class CounterBase { public abstract void Increment(); public abstract void Decrement(); }
運行結(jié)果如下圖所示,與預(yù)期結(jié)果相符。
1.11 使用Monitor類鎖定資源#
Monitor類主要用于線程同步中,?lock關(guān)鍵字是對Monitor類的一個封裝,其封裝結(jié)構(gòu)如下代碼所示。
Copy
try { Monitor.Enter(obj); dosomething(); } catch(Exception ex) { } finally { Monitor.Exit(obj); }
以下代碼演示了使用Monitor.TyeEnter()方法避免資源死鎖和使用lock發(fā)生資源死鎖的場景。
Copy
static void Main(string[] args) { object lock1 = new object(); object lock2 = new object(); new Thread(() => LockTooMuch(lock1, lock2)).Start(); lock (lock2) { Thread.Sleep(1000); Console.WriteLine("Monitor.TryEnter可以不被阻塞, 在超過指定時間后返回false"); // 如果5S不能進入同步塊,那么返回。 // 因為前面的lock鎖定了 lock2變量 而LockTooMuch()一開始鎖定了lock1 所以這個同步塊無法獲取 lock1 而LockTooMuch方法內(nèi)也不能獲取lock2 // 只能等待TryEnter超時 釋放 lock2 LockTooMuch()才會是釋放 lock1 if (Monitor.TryEnter(lock1, TimeSpan.FromSeconds(5))) { Console.WriteLine("獲取保護資源成功"); } else { Console.WriteLine("獲取資源超時"); } } new Thread(() => LockTooMuch(lock1, lock2)).Start(); Console.WriteLine("----------------------------------"); lock (lock2) { Console.WriteLine("這里會發(fā)生資源死鎖"); Thread.Sleep(1000); // 這里必然會發(fā)生死鎖 // 本同步塊 鎖定了 lock2 無法得到 lock1 // 而 LockTooMuch 鎖定了 lock1 無法得到 lock2 lock (lock1) { // 該語句永遠都不會執(zhí)行 Console.WriteLine("獲取保護資源成功"); } } } static void LockTooMuch(object lock1, object lock2) { lock (lock1) { Thread.Sleep(1000); lock (lock2) ; } }
運行結(jié)果如下圖所示,因為使用Monitor.TryEnter()方法在超時以后會返回,不會阻塞線程,所以沒有發(fā)生死鎖。而第二段代碼中l(wèi)ock沒有超時返回的功能,導(dǎo)致資源死鎖,同步塊中的代碼永遠不會被執(zhí)行。
1.12 多線程中處理異常#
在多線程中處理異常應(yīng)當(dāng)使用就近原則,在哪個線程發(fā)生異常那么所在的代碼塊一定要有相應(yīng)的異常處理。否則可能會導(dǎo)致程序崩潰、數(shù)據(jù)丟失。
主線程中使用try/catch語句是不能捕獲創(chuàng)建線程中的異常。但是萬一遇到不可預(yù)料的異常,可通過監(jiān)聽AppDomain.CurrentDomain.UnhandledException事件來進行捕獲和異常處理。
演示代碼如下所示,異常處理 1 和 異常處理 2 能正常被執(zhí)行,而異常處理 3 是無效的。
Copy
static void Main(string[] args) { // 啟動線程,線程代碼中進行異常處理 var t = new Thread(FaultyThread); t.Start(); t.Join(); // 捕獲全局異常 AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException; t = new Thread(BadFaultyThread); t.Start(); t.Join(); // 線程代碼中不進行異常處理,嘗試在主線程中捕獲 AppDomain.CurrentDomain.UnhandledException -= CurrentDomain_UnhandledException; try { t = new Thread(BadFaultyThread); t.Start(); } catch (Exception ex) { // 永遠不會運行 Console.WriteLine($"異常處理 3 : {ex.Message}"); } } private static void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e) { Console.WriteLine($"異常處理 2 :{(e.ExceptionObject as Exception).Message}"); } static void BadFaultyThread() { Console.WriteLine("有異常的線程已啟動..."); Thread.Sleep(TimeSpan.FromSeconds(2)); throw new Exception("Boom!"); } static void FaultyThread() { try { Console.WriteLine("有異常的線程已啟動..."); Thread.Sleep(TimeSpan.FromSeconds(1)); throw new Exception("Boom!"); } catch (Exception ex) { Console.WriteLine($"異常處理 1 : {ex.Message}"); } }
運行結(jié)果如下圖所示,與預(yù)期結(jié)果一致。
參考書籍
本文主要參考了以下幾本書,在此對這些作者表示由衷的感謝你們提供了這么好的資料。
線程基礎(chǔ)這一章節(jié)終于整理完了,是筆者學(xué)習(xí)過程中的筆記和思考。計劃按照《Multithreading with C# Cookbook Second Edition》這本書的結(jié)構(gòu),一共更新十二個章節(jié),先立個Flag。
源碼下載點擊鏈接?示例源碼下載
筆者水平有限,如果錯誤歡迎各位批評指正!
總結(jié)
以上是生活随笔為你收集整理的C#多线程编程系列(二)- 线程基础的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 连上WiFi就能打电话!上海电信打通全球
- 下一篇: 第十六节: EF的CodeFirst模式