C# 異步多線程入門到精通之Thread篇

上一篇:C# 異步多線程入門基礎
下一篇:C# 異步多線程入門到精通之ThreadPool篇

Thread API

這裡對 Thread 的一些常用 API 進行介紹,使用一些案例進行說明。由於 Thread 的不可控與效率問題,Thread 現在已經不常用瞭,這裡介紹一些 API ,想更深入的同學可以繼續研究研究。

Instance

首先看 Thread 的構造函數,有 ThreadStart 、ParameterizedThreadStart 、maxStackSize 類型的參數,這三個常用的也就 ThreadStart ,其他兩個可以作為瞭解。

在這裡插入圖片描述

分別 F12 查看 ThreadStart、ParameterizedThreadStart ,可以看到 ThreadStart 是無參數類型的委托、ParameterizedThreadStart 是有參數類型的委托。maxStackSize 是指定線程占用的最大內存數。

在這裡插入圖片描述

在這裡插入圖片描述

在這裡插入圖片描述

接著我們創建一個簡單的案例,啟動一個線程,模擬做一些任務行。如下代碼

Console.WriteLine($"Main 方法開始,ThreadId:{Thread.CurrentThread.ManagedThreadId}");

ThreadStart threadStart = () =>
{
    Console.WriteLine($"Task Start ThreadId:{Thread.CurrentThread.ManagedThreadId}");
    // 做一些任務


    Console.WriteLine($"Task End ThreadId:{Thread.CurrentThread.ManagedThreadId}");
};
Thread thread = new Thread(threadStart);
thread.Start();

Console.WriteLine($"Main 方法結束,ThreadId:{Thread.CurrentThread.ManagedThreadId}");

Console.ReadLine();

在這裡插入圖片描述

啟動程序,可以看到線程 1(主線程),沒有等待線程 3(子線程)執行完成匿名方法內的任務,再執行 Main 結束這段代碼。如果使用的是 winform 是不會卡界面的。

這就是異步多線程,異步在於線程 1 並沒有等待線程 3 執行完成任務,再執行線程 1 內的下一行,而是讓線程 3 在不影響線程 1 執行任務的情況下執行,這就是異步。多線程在於我們啟動瞭一個線程 3(子線程),在 Main 方法由線程1(子線程)與線程 3(主線程)一起完成 Main 方法內的代碼,這就是多線程。

在這裡插入圖片描述

說到委托可會有小夥伴發出疑問,為啥不用 Action ?

因為在這個版本還沒有 Action、Func,這是在 .Net 3.0 時代的產物,Action、Func 的出現就是為瞭統一,也是為瞭解決此類問題。

在 dotnet 框架,也建議最好使用 Action、Func,所以,在這使用 Action 是不可以的。如下

Console.WriteLine($"Main 方法開始,ThreadId:{Thread.CurrentThread.ManagedThreadId}");

Action action = () =>
{
    Console.WriteLine($"Task Start ThreadId:{Thread.CurrentThread.ManagedThreadId}");
    // 做一些任務


    Console.WriteLine($"Task End ThreadId:{Thread.CurrentThread.ManagedThreadId}");
};

ThreadStart threadStart = action;

Thread thread = new Thread(threadStart);
thread.Start();

Console.WriteLine($"Main 方法結束,ThreadId:{Thread.CurrentThread.ManagedThreadId}");

Console.ReadLine();

在這裡插入圖片描述

Suspend、Resume

Suspend 掛起、Resume 喚醒,這兩個是一對相互對應的 API,使用時這兩個容易產生死鎖,其實在實際中也是不應該使用的,.NET 框架已經拋棄瞭,說的很清楚瞭。

為什麼會死鎖呢?比如你開啟瞭一個子線程 01,對 A 文件進行讀寫操作,此時你對子線程 01 進行瞭掛起。當你另外一個線程對 02 A 文件進行操作時,此時提示會 A 文件被占用,就行形成死鎖。

在這裡插入圖片描述

在這裡插入圖片描述

Abort、ResetAbort

Abort 銷毀,很多人在使用,這種是拋異常方式,使子線程銷毀結束。這個功能也比較雞肋,Abort 時子線程並不能立即停止,往往會有一些延遲,那這個銷毀有時也不能達到我們可控的效果。

比如,在一個方法內開瞭一個子線程進行數據計算,但執行的時間太長瞭,我們等待瞭 5000 ms,此時 Abort 子線程,是不能立馬讓子線程停止計算,而是可能要等一會才能結束子線程。

比如,發出的動作,可能收不回來。查詢數據庫來說,當一個查庫命令發送到數據庫,我們在C# 執行瞭 Abort,但查庫這個命令是收不回來的,因為他是在數據庫層面,當數據庫查詢完成隻是沒有接收響應的線程罷瞭。

在這裡插入圖片描述

Abort 不建議使用,如果使用,一定要 try catch 一下。

Console.WriteLine($"Main 方法開始,ThreadId:{Thread.CurrentThread.ManagedThreadId}");

ThreadStart threadStart = () =>
{
    Console.WriteLine($"Task Start ThreadId:{Thread.CurrentThread.ManagedThreadId}");
    // 做一些任務

    Console.WriteLine($"Task End ThreadId:{Thread.CurrentThread.ManagedThreadId}");
};
Thread thread = new Thread(threadStart);

thread.Start();

try
{
    thread.Abort(); // 銷毀,方式是拋異常,不一定及時
}
catch (Exception ex)
{
    //Thread.ResetAbort(); // 取消異常
}

Console.WriteLine($"Main 方法結束,ThreadId:{Thread.CurrentThread.ManagedThreadId}");

Console.ReadLine();

在這裡插入圖片描述

Suspend、Resume、Abort 這幾個方法不建議使用,操作線程暫停、銷毀或者其他操作都是不可控的,應為線程本身是操作系統的, CPU 分時分片會按照自己的規則進行運行,此時已經不是程序可以進行控的瞭。 既然設計瞭 Thread 不可能一無是處,接下來我們說些有用的

Join

線程等待 ,Join 可以一直等,也可以設置超時,超時就是等待一定時間,就不等瞭。等待的過程中主線程處於閑置狀態等著子線程完成任務。如果是 winform 是會卡界面的,主線程等待也是一種工作。

例如:threadStart 我們模擬任務耗時 5 秒,在 thread.Start() 任務開始後,使用 thread.Join() 等著子線程完成工作

Console.WriteLine($"Main 方法開始,ThreadId:{Thread.CurrentThread.ManagedThreadId},DateTime:{DateTime.Now.ToLongTimeString()}");

ThreadStart threadStart = () =>
{
    Console.WriteLine($"Task Start ThreadId:{Thread.CurrentThread.ManagedThreadId},DateTime:{DateTime.Now.ToLongTimeString()}");
    // 做一些任務
    Thread.Sleep(5 * 1000); // 模擬任務耗時 5 秒

    Console.WriteLine($"Task End ThreadId:{Thread.CurrentThread.ManagedThreadId},DateTime:{DateTime.Now.ToLongTimeString()}");
};

Thread thread = new Thread(threadStart);
thread.Start();

thread.Join();

Console.WriteLine($"Main 方法結束,ThreadId:{Thread.CurrentThread.ManagedThreadId},DateTime:{DateTime.Now.ToLongTimeString()}");

Console.ReadLine();

啟動程序,可以看到是我們想要的結果(與同步執行一樣),主線程 1 一直等著 子線程 3 完成執行的任務。如果是 winform 是會卡界面的,雖然 thread.Join() 主線程 1 會等著子線程 3 完成工作,但主線程 1 等著也是一種工作。

在這裡插入圖片描述

接著我們看下超時等待,Join 的重載方法

例如:threadStart 我們模擬任務耗時 5 秒,在 thread.Start() 任務開始後,使用 thread.Join(3*1000) ,讓主線程最多等子線程 3 秒,如果 3 秒子線程還未完成任務,就不等待瞭

Console.WriteLine($"Main 方法開始,ThreadId:{Thread.CurrentThread.ManagedThreadId},DateTime:{DateTime.Now.ToLongTimeString()}");

ThreadStart threadStart = () =>
{
    Console.WriteLine($"Task Start ThreadId:{Thread.CurrentThread.ManagedThreadId},DateTime:{DateTime.Now.ToLongTimeString()}");
    // 做一些任務
    Thread.Sleep(5 * 1000); // 模擬任務耗時 5 秒

    Console.WriteLine($"Task End ThreadId:{Thread.CurrentThread.ManagedThreadId},DateTime:{DateTime.Now.ToLongTimeString()}");
};

Thread thread = new Thread(threadStart);
thread.Start();

thread.Join(3 * 1000);

Console.WriteLine($"Main 方法結束,ThreadId:{Thread.CurrentThread.ManagedThreadId},DateTime:{DateTime.Now.ToLongTimeString()}");

Console.ReadLine();

啟動程序,主線程 1 開始任務,子線程 3 也開始任務,當子線程執行 3 s 後(期間主線程 1 在等待),主線程 3 開始執行任務瞭。

在這裡插入圖片描述

註意:thread.Join(n * 1000) 並不是一定會等待那麼長時間,而是最多等待,期間子線程任務執行完成後,就不等待瞭。

例如:threadStart 任務方法模擬 5 s,thread.Join(7 * 1000) 主線程等待 7 s

Console.WriteLine($"Main 方法開始,ThreadId:{Thread.CurrentThread.ManagedThreadId},DateTime:{DateTime.Now.ToLongTimeString()}");

ThreadStart threadStart = () =>
{
    Console.WriteLine($"Task Start ThreadId:{Thread.CurrentThread.ManagedThreadId},DateTime:{DateTime.Now.ToLongTimeString()}");
    // 做一些任務
    Thread.Sleep(5 * 1000); // 模擬任務耗時 5 秒

    Console.WriteLine($"Task End ThreadId:{Thread.CurrentThread.ManagedThreadId},DateTime:{DateTime.Now.ToLongTimeString()}");
};

Thread thread = new Thread(threadStart);
thread.Start();

thread.Join(7 * 1000);

Console.WriteLine($"Main 方法結束,ThreadId:{Thread.CurrentThread.ManagedThreadId},DateTime:{DateTime.Now.ToLongTimeString()}");

Console.ReadLine();

在這裡插入圖片描述

ThreadState

線程狀態,ThreadState 也可以做線程等待,等待的過程中主線程處於閑置狀態等著子線程完成任務。如果是 winform 是會卡界面的,主線程等待也是一種工作。

Console.WriteLine($"Main 方法開始,ThreadId:{Thread.CurrentThread.ManagedThreadId},DateTime:{DateTime.Now.ToLongTimeString()}");

ThreadStart threadStart = () =>
{
    Console.WriteLine($"Task Start ThreadId:{Thread.CurrentThread.ManagedThreadId},DateTime:{DateTime.Now.ToLongTimeString()}");
    // 做一些任務
    Thread.Sleep(5 * 1000); // 模擬任務耗時 5 秒

    Console.WriteLine($"Task End ThreadId:{Thread.CurrentThread.ManagedThreadId},DateTime:{DateTime.Now.ToLongTimeString()}");
};

Thread thread = new Thread(threadStart);
thread.Start();

while (thread.ThreadState != ThreadState.Stopped)
{
    Thread.Sleep(200); // 當前線程休息 200 毫秒
}

Console.WriteLine($"Main 方法結束,ThreadId:{Thread.CurrentThread.ManagedThreadId},DateTime:{DateTime.Now.ToLongTimeString()}");

Console.ReadLine();

在這裡插入圖片描述

Sleep

線程暫停,Sleep 當前線程暫停。如果是 winform 是會卡界面的,當 Sleep 時,CPU 分片就交出去瞭,主線程並不在工作狀態。

Console.WriteLine($"Main 方法開始,ThreadId:{Thread.CurrentThread.ManagedThreadId},DateTime:{DateTime.Now.ToLongTimeString()}");

Thread.Sleep(5 * 1000); // 模擬任務耗時 5 秒

Console.WriteLine($"Main 方法結束,ThreadId:{Thread.CurrentThread.ManagedThreadId},DateTime:{DateTime.Now.ToLongTimeString()}");

Console.ReadLine();

在這裡插入圖片描述

IsBackground

是否是後臺線程,當實例 Thread 時,默認是前臺線程(IsBackground == false )。前臺線程一定要任務完成,才會讓進程退出。後臺線程(IsBackground == true)會隨著進程的結束而結束,無論子線程任務是否完成。

前臺線程,意思也就是,當我們啟動一個程序,當關閉程序時,如果還有子線程執行任務,當前進程是不會退出的,會等待著子進程將任務執行完成,也就是會阻止進程結束,反之亦然。

例如:前臺線程,啟動控制臺後,主線程執行完任務後,會等待子線程任務完成(5s)後,窗口才會被關閉

static void Main(string[] args)
{
    Console.WriteLine($"Main 方法開始,ThreadId:{Thread.CurrentThread.ManagedThreadId},DateTime:{DateTime.Now.ToLongTimeString()}");

    ThreadStart threadStart = () =>
    {
        Console.WriteLine($"Task Start ThreadId:{Thread.CurrentThread.ManagedThreadId},DateTime:{DateTime.Now.ToLongTimeString()}");
        // 做一些任務
        Thread.Sleep(5 * 1000); // 模擬任務耗時 5 秒

        Console.WriteLine($"Task End ThreadId:{Thread.CurrentThread.ManagedThreadId},DateTime:{DateTime.Now.ToLongTimeString()}");
    };

    Thread thread = new Thread(threadStart);
    thread.Start();

    while (thread.ThreadState != ThreadState.Stopped)
    {
        Thread.Sleep(200); // 當前線程休息 200 毫秒
    }

    Console.WriteLine($"Main 方法結束,ThreadId:{Thread.CurrentThread.ManagedThreadId},DateTime:{DateTime.Now.ToLongTimeString()}");
}

例如:後臺線程,啟動控制臺後,主線程任務執行完畢後,窗口會立馬被關閉

static void Main(string[] args)
{
    Console.WriteLine($"Main 方法開始,ThreadId:{Thread.CurrentThread.ManagedThreadId},DateTime:{DateTime.Now.ToLongTimeString()}");

    ThreadStart threadStart = () =>
    {
        Console.WriteLine($"Task Start ThreadId:{Thread.CurrentThread.ManagedThreadId},DateTime:{DateTime.Now.ToLongTimeString()}");
        // 做一些任務
        Thread.Sleep(5 * 1000); // 模擬任務耗時 5 秒

        Console.WriteLine($"Task End ThreadId:{Thread.CurrentThread.ManagedThreadId},DateTime:{DateTime.Now.ToLongTimeString()}");
    };

    Thread thread = new Thread(threadStart);
    thread.IsBackground = true;
    thread.Start();

    Console.WriteLine($"thread IsBackground:{thread.IsBackground},DateTime:{DateTime.Now.ToLongTimeString()}");

    Console.WriteLine($"Main 方法結束,ThreadId:{Thread.CurrentThread.ManagedThreadId},DateTime:{DateTime.Now.ToLongTimeString()}");
}

Priority

線程可以設置優先級,當線程從高到低分配瞭優先級,在向 CPU 申請線程時會優先分配。但是這個功能也比較雞肋,對於 CPU 而言,當他們同時過來,隻是會為優先級高的先分進行分片,但優先級低的並不是不會分配,也不代表優先級高的就會先執行完成,這也取決執行的任務量。其實優先級也沒什麼用,多線程本來就是無序的。

Console.WriteLine($"Main 方法開始,ThreadId:{Thread.CurrentThread.ManagedThreadId},DateTime:{DateTime.Now.ToLongTimeString()}");

ThreadStart threadStart = () =>
{
    Console.WriteLine($"Task Start ThreadId:{Thread.CurrentThread.ManagedThreadId},DateTime:{DateTime.Now.ToLongTimeString()}");
    // 做一些任務
    Thread.Sleep(5 * 1000); // 模擬任務耗時 5 秒

    Console.WriteLine($"Task End ThreadId:{Thread.CurrentThread.ManagedThreadId},DateTime:{DateTime.Now.ToLongTimeString()}");
};

Thread thread = new Thread(threadStart);
thread.Priority = ThreadPriority.Highest;// CPU 會先執行,不代表 Highest 就最優先
thread.Start();

Console.WriteLine($"thread IsBackground:{thread.IsBackground},DateTime:{DateTime.Now.ToLongTimeString()}");

Console.WriteLine($"Main 方法結束,ThreadId:{Thread.CurrentThread.ManagedThreadId},DateTime:{DateTime.Now.ToLongTimeString()}");

總結

其實現在來說 ,1.0 時代的 Thread 已經沒有什麼優勢,現在 Thread 唯一有意義的就是 IsBackground = false,這個前線程(前臺線程會阻礙進程的退出),後續的多線程設計都是後臺線程,沒有前臺線程這個功能設計。

到此這篇關於C# 異步多線程入門到精通之Thread篇的文章就介紹到這瞭,更多相關C# Thread內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: