C 多線程編程系列(二) 線程基礎 [TOC] 1.1 簡介 線程基礎主要包括線程創建、掛起、等待和終止線程。關於更多的線程的底層實現,CPU時間片輪轉等等的知識,可以參考 一書中關於進程和線程的章節,本文不過多贅述。 1.2 創建線程 在C 語言中,創建線程是一件非常簡單的事情;它只需要用到 命名 ...
目錄
C#多線程編程系列(二)- 線程基礎
1.1 簡介
線程基礎主要包括線程創建、掛起、等待和終止線程。關於更多的線程的底層實現,CPU時間片輪轉等等的知識,可以參考《深入理解電腦系統》
一書中關於進程和線程的章節,本文不過多贅述。
1.2 創建線程
在C#語言中,創建線程是一件非常簡單的事情;它只需要用到 System.Threading
命名空間,其中主要使用Thread
類來創建線程。
演示代碼如下所示:
using System;
using System.Threading; // 創建線程需要用到的命名空間
namespace Recipe1
{
class Program
{
static void Main(string[] args)
{
// 1.創建一個線程 PrintNumbers為該線程所需要執行的方法
Thread t = new Thread(PrintNumbers);
// 2.啟動線程
t.Start();
// 主線程也運行PrintNumbers方法,方便對照
PrintNumbers();
// 暫停一下
Console.ReadKey();
}
static void PrintNumbers()
{
// 使用Thread.CurrentThread.ManagedThreadId 可以獲取當前運行線程的唯一標識,通過它來區別線程
Console.WriteLine($"線程:{Thread.CurrentThread.ManagedThreadId} 開始列印...");
for (int i = 0; i < 10; i++)
{
Console.WriteLine($"線程:{Thread.CurrentThread.ManagedThreadId} 列印:{i}");
}
}
}
}
運行結果如下圖所示,我們可以通過運行結果得知上面的代碼創建了一個線程,然後主線程和創建的線程交叉輸出結果,這說明PrintNumbers
方法同時運行在主線程和另外一個線程中。
1.3 暫停線程
暫停線程這裡使用的方式是通過Thread.Sleep
方法,如果線程執行Thread.Sleep
方法,那麼操作系統將在指定的時間內不為該線程分配任何時間片。如果Sleep時間100ms那麼操作系統將至少讓該線程睡眠100ms或者更長時間,所以Thread.Sleep
方法不能作為高精度的計時器使用。
演示代碼如下所示:
using System;
using System.Threading; // 創建線程需要用到的命名空間
namespace Recipe2
{
class Program
{
static void Main(string[] args)
{
// 1.創建一個線程 PrintNumbers為該線程所需要執行的方法
Thread t = new Thread(PrintNumbersWithDelay);
// 2.啟動線程
t.Start();
// 暫停一下
Console.ReadKey();
}
static void PrintNumbersWithDelay()
{
Console.WriteLine($"線程:{Thread.CurrentThread.ManagedThreadId} 開始列印... 現在時間{DateTime.Now.ToString("HH:mm:ss.ffff")}");
for (int i = 0; i < 10; i++)
{
//3. 使用Thread.Sleep方法來使當前線程睡眠,TimeSpan.FromSeconds(2)表示時間為 2秒
Thread.Sleep(TimeSpan.FromSeconds(2));
Console.WriteLine($"線程:{Thread.CurrentThread.ManagedThreadId} 列印:{i} 現在時間{DateTime.Now.ToString("HH:mm:ss.ffff")}");
}
}
}
}
運行結果如下圖所示,通過下圖可以確定上面的代碼是有效的,通過Thread.Sleep
方法,使線程休眠了2秒左右,但是並不是特別精確的2秒。驗證了上面的說法,它的睡眠是至少讓線程睡眠多長時間,而不是一定多長時間。
1.4 線程等待
在本章中,線程等待使用的是Join
方法,該方法將暫停執行當前線程,直到所等待的另一個線程終止。在簡單的線程同步中會使用到,但它比較簡單,不作過多介紹。
演示代碼如下所示:
class Program
{
static void Main(string[] args)
{
Console.WriteLine($"-------開始執行 現在時間{DateTime.Now.ToString("HH:mm:ss.ffff")}-------");
// 1.創建一個線程 PrintNumbersWithDelay為該線程所需要執行的方法
Thread t = new Thread(PrintNumbersWithDelay);
// 2.啟動線程
t.Start();
// 3.等待線程結束
t.Join();
Console.WriteLine($"-------執行完畢 現在時間{DateTime.Now.ToString("HH:mm:ss.ffff")}-------");
// 暫停一下
Console.ReadKey();
}
static void PrintNumbersWithDelay()
{
Console.WriteLine($"線程:{Thread.CurrentThread.ManagedThreadId} 開始列印... 現在時間{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} 現在時間{DateTime.Now.ToString("HH:mm:ss.ffff")}");
}
}
}
運行結果如下圖所示,開始執行和執行完畢兩條信息由主線程列印;根據其輸出的順序可見主線程是等待另外的線程結束後才輸出執行完畢這條信息。
1.5 終止線程
終止線程使用的方法是Abort
方法,當該方法被執行時,將嘗試銷毀該線程。通過引發ThreadAbortException
異常使線程被銷毀。但一般不推薦使用該方法,原因有以下幾點。
- 使用
Abort
方法只是嘗試銷毀該線程,但不一定能終止線程。- 如果被終止的線程在執行lock內的代碼,那麼終止線程會造成線程不安全。
- 線程終止時,CLR會保證自己內部的數據結構不會損壞,但是BCL不能保證。
基於以上原因不推薦使用Abort
方法,在實際項目中一般使用CancellationToken
來終止線程。
演示代碼如下所示:
static void Main(string[] args)
{
Console.WriteLine($"-------開始執行 現在時間{DateTime.Now.ToString("HH:mm:ss.ffff")}-------");
// 1.創建一個線程 PrintNumbersWithDelay為該線程所需要執行的方法
Thread t = new Thread(PrintNumbersWithDelay);
// 2.啟動線程
t.Start();
// 3.主線程休眠6秒
Thread.Sleep(TimeSpan.FromSeconds(6));
// 4.終止線程
t.Abort();
Console.WriteLine($"-------執行完畢 現在時間{DateTime.Now.ToString("HH:mm:ss.ffff")}-------");
// 暫停一下
Console.ReadKey();
}
static void PrintNumbersWithDelay()
{
Console.WriteLine($"線程:{Thread.CurrentThread.ManagedThreadId} 開始列印... 現在時間{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} 現在時間{DateTime.Now.ToString("HH:mm:ss.ffff")}");
}
}
運行結果如下圖所示,啟動所創建的線程3後,6秒鐘主線程調用了Abort
方法,線程3沒有繼續執行便結束了;與預期的結果一致。
1.6 檢測線程狀態
線程的狀態可通過訪問ThreadState
屬性來檢測,ThreadState
是一個枚舉類型,一共有10種狀態,狀態具體含義如下表所示。
成員名稱 | 說明 |
---|---|
Aborted | 線程處於 Stopped 狀態中。 |
AbortRequested | 已對線程調用了 Thread.Abort 方法,但線程尚未收到試圖終止它的掛起的 System.Threading.ThreadAbortException。 |
Background | 線程正作為後臺線程執行(相對於前臺線程而言)。此狀態可以通過設置 Thread.IsBackground 屬性來控制。 |
Running | 線程已啟動,它未被阻塞,並且沒有掛起的 ThreadAbortException。 |
Stopped | 線程已停止。 |
StopRequested | 正在請求線程停止。這僅用於內部。 |
Suspended | 線程已掛起。 |
SuspendRequested | 正在請求線程掛起。 |
Unstarted | 尚未對線程調用 Thread.Start 方法。 |
WaitSleepJoin | 由於調用 Wait、Sleep 或 Join,線程已被阻止。 |
下表列出導致狀態更改的操作。
操作 | ThreadState |
---|---|
在公共語言運行庫中創建線程。 | Unstarted |
線程調用 Start | Unstarted |
線程開始運行。 | Running |
線程調用 Sleep | WaitSleepJoin |
線程對其他對象調用 Wait。 | WaitSleepJoin |
線程對其他線程調用 Join。 | WaitSleepJoin |
另一個線程調用 Interrupt | Running |
另一個線程調用 Suspend | SuspendRequested |
線程響應 Suspend 請求。 | Suspended |
另一個線程調用 Resume | Running |
另一個線程調用 Abort | AbortRequested |
線程響應 Abort 請求。 | Stopped |
線程被終止。 | Stopped |
演示代碼如下所示:
static void Main(string[] args)
{
Console.WriteLine("開始執行...");
Thread t = new Thread(PrintNumbersWithStatus);
Thread t2 = new Thread(DoNothing);
// 使用ThreadState查看線程狀態 此時線程未啟動,應為Unstarted
Console.WriteLine($"Check 1 :{t.ThreadState}");
t2.Start();
t.Start();
// 線程啟動, 狀態應為 Running
Console.WriteLine($"Check 2 :{t.ThreadState}");
// 由於PrintNumberWithStatus方法開始執行,狀態為Running
// 但是經接著會執行Thread.Sleep方法 狀態會轉為 WaitSleepJoin
for (int i = 1; i < 30; i++)
{
Console.WriteLine($"Check 3 : {t.ThreadState}");
}
// 延時一段時間,方便查看狀態
Thread.Sleep(TimeSpan.FromSeconds(6));
// 終止線程
t.Abort();
Console.WriteLine("t線程被終止");
// 由於該線程是被Abort方法終止 所以狀態為 Aborted或AbortRequested
Console.WriteLine($"Check 4 : {t.ThreadState}");
// 該線程正常執行結束 所以狀態為Stopped
Console.WriteLine($"Check 5 : {t2.ThreadState}");
Console.ReadKey();
}
static void DoNothing()
{
Thread.Sleep(TimeSpan.FromSeconds(2));
}
static void PrintNumbersWithStatus()
{
Console.WriteLine("t線程開始執行...");
// 線上程內部,可通過Thread.CurrentThread拿到當前線程Thread對象
Console.WriteLine($"Check 6 : {Thread.CurrentThread.ThreadState}");
for (int i = 1; i < 10; i++)
{
Thread.Sleep(TimeSpan.FromSeconds(2));
Console.WriteLine($"t線程輸出 :{i}");
}
}
運行結果如下圖所示,與預期的結果一致。
1.7 線程優先順序
Windows操作系統為搶占式多線程(Preemptive multithreaded)操作系統,是因為線程可在任何時間停止(被槍占)並調度另一個線程。
Windows操作系統中線程有0(最低) ~ 31(最高)
的優先順序,而優先順序越高所能占用的CPU時間就越多,確定某個線程所處的優先順序需要考慮進程優先順序和相對線程優先順序兩個優先順序。
- 進程優先順序:Windows支持6個進程優先順序,分別是
Idle、Below Normal、Normal、Above normal、High 和Realtime
。預設為Normal
。- 相對線程優先順序:相對線程優先順序是相對於進程優先順序的,因為進程包含了線程。Windows支持7個相對線程優先順序,分別是
Idle、Lowest、Below Normal、Normal、Above Normal、Highest 和 Time-Critical
.預設為Normal
。
下表總結了進程的優先順序和線程的相對優先順序與優先順序(0~31)的映射關係。粗體為相對線程優先順序,斜體為進程優先順序。
Idle | Below Normal | Normal | Above Normal | High | Realtime | |
---|---|---|---|---|---|---|
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#程式中,可更改線程的相對優先順序,需要設置Thread
的Priority
屬性,可設置為ThreadPriority
枚舉類型的五個值之一:Lowest、BelowNormal、Normal、AboveNormal 或 Highest
。CLR為自己保留了Idle
和Time-Critical
優先順序,程式中不可設置。
演示代碼如下所示。
static void Main(string[] args)
{
Console.WriteLine($"當前線程優先順序: {Thread.CurrentThread.Priority} \r\n");
// 第一次測試,在所有核心上運行
Console.WriteLine("運行在所有空閑的核心上");
RunThreads();
Thread.Sleep(TimeSpan.FromSeconds(2));
// 第二次測試,在單個核心上運行
Console.WriteLine("\r\n運行在單個核心上");
// 設置在單個核心上運行
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 = "線程二";
// 設置優先順序和啟動線程
threadOne.Priority = ThreadPriority.Highest;
threadTwo.Priority = ThreadPriority.Lowest;
threadOne.Start();
threadTwo.Start();
// 延時2秒 查看結果
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} 優先順序為 {Thread.CurrentThread.Priority,11} 計數為 = {counter,13:N0}");
}
}
運行結果如下圖所示。Highest
占用的CPU時間明顯多於Lowest
。當程式運行在所有核心上時,線程可以在不同核心同時運行,所以Highest
和Lowest
差距會小一些。
1.8 前臺線程和後臺線程
在CLR中,線程要麼是前臺線程,要麼就是後臺線程。當一個進程的所有前臺線程停止運行時,CLR將強制終止仍在運行的任何後臺線程,不會拋出異常。
在C#中可通過Thread
類中的IsBackground
屬性來指定是否為後臺線程。線上程生命周期中,任何時候都可從前臺線程變為後臺線程。線程池中的線程預設為後臺線程。
演示代碼如下所示。
static void Main(string[] args)
{
var sampleForeground = new ThreadSample(10);
var sampleBackground = new ThreadSample(20);
var threadPoolBackground = new ThreadSample(20);
// 預設創建為前臺線程
var threadOne = new Thread(sampleForeground.CountNumbers);
threadOne.Name = "前臺線程";
var threadTwo = new Thread(sampleBackground.CountNumbers);
threadTwo.Name = "後臺線程";
// 設置IsBackground屬性為 true 表示後臺線程
threadTwo.IsBackground = true;
// 線程池內的線程預設為 後臺線程
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}");
}
}
}
運行結果如下圖所示。當前臺線程10次迴圈結束以後,創建的後臺線程和線程池線程都會被CLR強制結束。
1.9 向線程傳遞參數
向線程中傳遞參數常用的有三種方法,構造函數傳值、Start方法傳值和Lambda表達式傳值,一般常用Start方法來傳值。
演示代碼如下所示,通過三種方式來傳遞參數,告訴線程中的迴圈最終需要迴圈幾次。
static void Main(string[] args)
{
// 第一種方法 通過構造函數傳值
var sample = new ThreadSample(10);
var threadOne = new Thread(sample.CountNumbers);
threadOne.Name = "ThreadOne";
threadOne.Start();
threadOne.Join();
Console.WriteLine("--------------------------");
// 第二種方法 使用Start方法傳值
// Count方法 接收一個Object類型參數
var threadTwo = new Thread(Count);
threadTwo.Name = "ThreadTwo";
// Start方法中傳入的值 會傳遞到 Count方法 Object參數上
threadTwo.Start(8);
threadTwo.Join();
Console.WriteLine("--------------------------");
// 第三種方法 Lambda表達式傳值
// 實際上是構建了一個匿名函數 通過函數閉包來傳值
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}");
}
}
}
運行結果如下圖所示,與預期結果相符。
1.10 C# Lock關鍵字的使用
在多線程的系統中,由於CPU的時間片輪轉等線程調度演算法的使用,容易出現線程安全問題。具體可參考《深入理解電腦系統》
一書相關的章節。
在C#中lock
關鍵字是一個語法糖,它將Monitor
封裝,給object加上一個互斥鎖,從而實現代碼的線程安全,Monitor
會在下一節中介紹。
對於lock
關鍵字還是Monitor
鎖定的對象,都必須小心選擇,不恰當的選擇可能會造成嚴重的性能問題甚至發生死鎖。以下有幾條關於選擇鎖定對象的建議。
- 同步鎖定的對象不能是值類型。因為使用值類型時會有裝箱的問題,裝箱後的就成了一個新的實例,會導致
Monitor.Enter()
和Monitor.Exit()
接收到不同的實例而失去關聯性- 避免鎖定
this、typeof(type)和string
。this
和typeof(type)
鎖定可能在其它不相干的代碼中會有相同的定義,導致多個同步塊互相阻塞。string
需要考慮字元串拘留的問題,如果同一個字元串常量在多個地方出現,可能引用的會是同一個實例。- 對象的選擇作用域儘可能剛好達到要求,使用靜態的、私有的變數。
以下演示代碼實現了多線程情況下的計數功能,一種實現是線程不安全的,會導致結果與預期不相符,但也有可能正確。另外一種使用了lock
關鍵字進行線程同步,所以它結果是一定的。
static void Main(string[] args)
{
Console.WriteLine("錯誤的多線程計數方式");
var c = new Counter();
// 開啟3個線程,使用沒有同步塊的計數方式對其進行計數
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();
// 因為多線程 線程搶占等原因 其結果是不一定的 碰巧可能為0
Console.WriteLine($"Total count: {c.Count}");
Console.WriteLine("--------------------------");
Console.WriteLine("正確的多線程計數方式");
var c1 = new CounterWithLock();
// 開啟3個線程,使用帶有lock同步塊的方式對其進行計數
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();
// 其結果是一定的 為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();
}
}
// 線程不安全的計數
class Counter : CounterBase
{
public int Count { get; private set; }
public override void Increment()
{
Count++;
}
public override void Decrement()
{
Count--;
}
}
// 線程安全的計數
class CounterWithLock : CounterBase
{
private readonly object _syncRoot = new Object();
public int Count { get; private set; }
public override void Increment()
{
// 使用Lock關鍵字 鎖定私有變數
lock (_syncRoot)
{
// 同步塊
Count++;
}
}
public override void Decrement()
{
lock (_syncRoot)
{
Count--;
}
}
}
abstract class CounterBase
{
public abstract void Increment();
public abstract void Decrement();
}
運行結果如下圖所示,與預期結果相符。
1.11 使用Monitor類鎖定資源
Monitor
類主要用於線程同步中, lock
關鍵字是對Monitor
類的一個封裝,其封裝結構如下代碼所示。
try
{
Monitor.Enter(obj);
dosomething();
}
catch(Exception ex)
{
}
finally
{
Monitor.Exit(obj);
}
以下代碼演示了使用Monitor.TyeEnter()
方法避免資源死鎖和使用lock
發生資源死鎖的場景。
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方法內也不能獲取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("這裡會發生資源死鎖");
Thread.Sleep(1000);
// 這裡必然會發生死鎖
// 本同步塊 鎖定了 lock2 無法得到 lock1
// 而 LockTooMuch 鎖定了 lock1 無法得到 lock2
lock (lock1)
{
// 該語句永遠都不會執行
Console.WriteLine("獲取保護資源成功");
}
}
}
static void LockTooMuch(object lock1, object lock2)
{
lock (lock1)
{
Thread.Sleep(1000);
lock (lock2) ;
}
}
運行結果如下圖所示,因為使用Monitor.TryEnter()
方法在超時以後會返回,不會阻塞線程,所以沒有發生死鎖。而第二段代碼中lock
沒有超時返回的功能,導致資源死鎖,同步塊中的代碼永遠不會被執行。
1.12 多線程中處理異常
在多線程中處理異常應當使用就近原則,在哪個線程發生異常那麼所在的代碼塊一定要有相應的異常處理。否則可能會導致程式崩潰、數據丟失。
主線程中使用try/catch
語句是不能捕獲創建線程中的異常。但是萬一遇到不可預料的異常,可通過監聽AppDomain.CurrentDomain.UnhandledException
事件來進行捕獲和異常處理。
演示代碼如下所示,異常處理 1 和 異常處理 2 能正常被執行,而異常處理 3 是無效的。
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}");
}
}
運行結果如下圖所示,與預期結果一致。
參考書籍
本文主要參考了以下幾本書,在此對這些作者表示由衷的感謝你們提供了這麼好的資料。
- 《CLR via C#》
- 《C# in Depth Third Edition》
- 《Essential C# 6.0》
- 《Multithreading with C# Cookbook Second Edition》
線程基礎這一章節終於整理完了,是筆者學習過程中的筆記和思考。計劃按照《Multithreading with C# Cookbook Second Edition》這本書的結構,一共更新十二個章節,先立個Flag。
源碼下載點擊鏈接 示例源碼下載