C#多線程編程系列(二)- 線程基礎

来源:https://www.cnblogs.com/InCerry/archive/2018/08/01/9404030.html
-Advertisement-
Play Games

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

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。


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

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


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • #include #include //提供malloc()原型 #include //提供true false原型 #define MaxSize 10 #define ERROR -1 typedef struct SNode *Stack; typedef int ElementType ; ... ...
  • # mac離線安裝dockerhttps://download.docker.com/mac/stable/24312/Docker.dmg # windows離線安裝dockerhttp://mirrors.aliyun.com/docker- toolbox/windows/docker-too ...
  • 租車系統 測試類 抽象類Car 子類ren (只能載人) 子類wu (只能載貨) 子類quan (都能載) 運行截圖 ...
  • 一、背景   我們都知道http協議只能瀏覽器單方面向伺服器發起請求獲得響應,伺服器不能主動向瀏覽器推送消息。想要實現瀏覽器的主動推送有兩種主流實現方式: 輪詢:缺點很多,但是實現簡單 websocket:在瀏覽器和伺服器之間建立tcp連接,實現全雙工通信  &emsp ...
  • 協程 引子 之前我們學習了線程、進程的概念,瞭解了在操作系統中進程是資源分配的最小單位,線程是CPU調度的最小單位。按道理來說我們已經算是把cpu的利用率提高很多了。但是我們知道無論是創建多進程還是創建多線程來解決問題,都要消耗一定的時間來創建進程、創建線程、以及管理他們之間的切換。 隨著我們對於效 ...
  • ORM的優劣勢? https://www.cnblogs.com/huanhang/p/6054908.html 對象關係映射(Object Relational Mapping,簡稱ORM)模式是一種為瞭解決面向對象與關係資料庫存在的互不匹配的現象的技術。 簡單的說,ORM是通過使用描述對象和數據 ...
  • 一:有如下變數(tu是個元祖),請實現要求的功能 tu = ("alex", [11, 22, {"k1": 'v1', "k2": ["age", "name"], "k3": (11,22,33)}, 44]) a. 講述元祖的特性 b. 請問tu變數中的第一個元素 "alex" 是否可被修改? ...
  • 重載與重寫對比: 重載: 許可權修飾符(public private 預設):無關 方法名:重載的兩個方法的方法名必須相同 形參列表: 形參類型的順序不同 形參的個數不同 形參的類型不同 三者至少滿足一個 返回值類型: 重載與返回值類型無關 重寫: 許可權修飾符(public private 預設): ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...