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方法同時運行在主線程和另外一個線程中。

1533090931719

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秒。驗證了上面的說法,它的睡眠是至少讓線程睡眠多長時間,而不是一定多長時間。

1533091915863

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")}");
        }
    }
}

運行結果如下圖所示,開始執行和執行完畢兩條信息由主線程打印;根據其輸出的順序可見主線程是等待另外的線程結束后才輸出執行完畢這條信息。

1533095197008

1.5 終止線程

終止線程使用的方法是Abort方法,當該方法被執行時,將嘗試銷毀該線程。通過引發ThreadAbortException異常使線程被銷毀。但一般不推薦使用該方法,原因有以下幾點。

  1. 使用Abort方法只是嘗試銷毀該線程,但不一定能終止線程。
  2. 如果被終止的線程在執行lock內的代碼,那么終止線程會造成線程不安全。
  3. 線程終止時,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沒有繼續執行便結束了;與預期的結果一致。

1533096132246

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 由於調用 WaitSleepJoin,線程已被阻止。

下表列出導致狀態更改的操作。

操作 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}");
    }
}

運行結果如下圖所示,與預期的結果一致。

1533107472877

1.7 線程優先級

Windows操作系統為搶占式多線程(Preemptive multithreaded)操作系統,是因為線程可在任何時間停止(被槍占)並調度另一個線程。

Windows操作系統中線程有0(最低) ~ 31(最高)的優先級,而優先級越高所能占用的CPU時間就越多,確定某個線程所處的優先級需要考慮進程優先級相對線程優先級兩個優先級。

  1. 進程優先級:Windows支持6個進程優先級,分別是Idle、Below Normal、Normal、Above normal、High 和Realtime。默認為Normal
  2. 相對線程優先級:相對線程優先級是相對於進程優先級的,因為進程包含了線程。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#程序中,可更改線程的相對優先級,需要設置ThreadPriority屬性,可設置為ThreadPriority枚舉類型的五個值之一:Lowest、BelowNormal、Normal、AboveNormal 或 Highest。CLR為自己保留了IdleTime-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。當程序運行在所有核心上時,線程可以在不同核心同時運行,所以HighestLowest差距會小一些。

1533109869998

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強制結束。

1533116008700

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}");
        }
    }
}

運行結果如下圖所示,與預期結果相符。

1533124776052

1.10 C# Lock關鍵字的使用

在多線程的系統中,由於CPU的時間片輪轉等線程調度算法的使用,容易出現線程安全問題。具體可參考《深入理解計算機系統》一書相關的章節。

在C#中lock關鍵字是一個語法糖,它將Monitor封裝,給object加上一個互斥鎖,從而實現代碼的線程安全,Monitor會在下一節中介紹。

對於lock關鍵字還是Monitor鎖定的對象,都必須小心選擇,不恰當的選擇可能會造成嚴重的性能問題甚至發生死鎖。以下有幾條關於選擇鎖定對象的建議。

  1. 同步鎖定的對象不能是值類型。因為使用值類型時會有裝箱的問題,裝箱后的就成了一個新的實例,會導致Monitor.Enter()Monitor.Exit()接收到不同的實例而失去關聯性
  2. 避免鎖定this、typeof(type)和stringthistypeof(type)鎖定可能在其它不相干的代碼中會有相同的定義,導致多個同步塊互相阻塞。string需要考慮字符串拘留的問題,如果同一個字符串常量在多個地方出現,可能引用的會是同一個實例。
  3. 對象的選擇作用域盡可能剛好達到要求,使用靜態的、私有的變量。

以下演示代碼實現了多線程情況下的計數功能,一種實現是線程不安全的,會導致結果與預期不相符,但也有可能正確。另外一種使用了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();
}

運行結果如下圖所示,與預期結果相符。

1533126787553

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沒有超時返回的功能,導致資源死鎖,同步塊中的代碼永遠不會被執行。

1533127789225

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}");
    }
}

運行結果如下圖所示,與預期結果一致。

1533129416654

參考書籍

本文主要參考了以下幾本書,在此對這些作者表示由衷的感謝你們提供了這么好的資料。

  1. 《CLR via C#》
  2. 《C# in Depth Third Edition》
  3. 《Essential C# 6.0》
  4. 《Multithreading with C# Cookbook Second Edition》

線程基礎這一章節終於整理完了,是筆者學習過程中的筆記和思考。計划按照《Multithreading with C# Cookbook Second Edition》這本書的結構,一共更新十二個章節,先立個Flag。


源碼下載點擊鏈接 示例源碼下載

筆者水平有限,如果錯誤歡迎各位批評指正!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM