C#多線程系列之線程的創建和生命周期
1,獲取當前線程信息
Thread.CurrentThread
是一個 靜態的 Thread 類,Thread 的CurrentThread
屬性,可以獲取到當前運行線程的一些信息,其定義如下:
public static System.Threading.Thread CurrentThread { get; }
Thread 類有很多屬性和方法,這裡就不列舉瞭,後面的學習會慢慢熟悉更多 API 和深入瞭解使用。
這裡有一個簡單的示例:
static void Main(string[] args) { Thread thread = new Thread(OneTest); thread.Name = "Test"; thread.Start(); Console.ReadKey(); } public static void OneTest() { Thread thisTHread = Thread.CurrentThread; Console.WriteLine("線程標識:" + thisTHread.Name); Console.WriteLine("當前地域:" + thisTHread.CurrentCulture.Name); // 當前地域 Console.WriteLine("線程執行狀態:" + thisTHread.IsAlive); Console.WriteLine("是否為後臺線程:" + thisTHread.IsBackground); Console.WriteLine("是否為線程池線程"+thisTHread.IsThreadPoolThread); }
輸出
線程標識:Test 當前地域:zh-CN 線程執行狀態:True 是否為後臺線程:False 是否為線程池線程False
2,管理線程狀態
一般認為,線程有五種狀態:
新建(new 對象) 、就緒(等待CPU調度)、運行(CPU正在運行)、阻塞(等待阻塞、同步阻塞等)、死亡(對象釋放)。
理論的東西不說太多,直接擼代碼。
2.1 啟動與參數傳遞
新建線程簡直滾瓜爛熟,無非 new
一下,然後 Start()
。
Thread thread = new Thread();
Thread 的構造函數有四個:
public Thread(ParameterizedThreadStart start); public Thread(ThreadStart start); public Thread(ParameterizedThreadStart start, int maxStackSize); public Thread(ThreadStart start, int maxStackSize);
我們以啟動新的線程時傳遞參數來舉例,使用這四個構造函數呢?
2.1.1 ParameterizedThreadStart
ParameterizedThreadStart 是一個委托,構造函數傳遞的參數為需要執行的方法,然後在 Start
方法中傳遞參數。
需要註意的是,傳遞的參數類型為 object,而且隻能傳遞一個。
代碼示例如下:
static void Main(string[] args) { string myParam = "abcdef"; ParameterizedThreadStart parameterized = new ParameterizedThreadStart(OneTest); Thread thread = new Thread(parameterized); thread.Start(myParam); Console.ReadKey(); } public static void OneTest(object obj) { string str = obj as string; if (string.IsNullOrEmpty(str)) return; Console.WriteLine("新的線程已經啟動"); Console.WriteLine(str); }
2.1.2 使用靜態變量或類成員變量
此種方法不需要作為參數傳遞,各個線程共享堆棧。
優點是不需要裝箱拆箱,多線程可以共享空間;缺點是變量是大傢都可以訪問,此種方式在多線程競價時,可能會導致多種問題(可以加鎖解決)。
下面使用兩個變量實現數據傳遞:
class Program { private string A = "成員變量"; public static string B = "靜態變量"; static void Main(string[] args) { // 創建一個類 Program p = new Program(); Thread thread1 = new Thread(p.OneTest1); thread1.Name = "Test1"; thread1.Start(); Thread thread2 = new Thread(OneTest2); thread2.Name = "Test2"; thread2.Start(); Console.ReadKey(); } public void OneTest1() { Console.WriteLine("新的線程已經啟動"); Console.WriteLine(A); // 本身對象的其它成員 } public static void OneTest2() { Console.WriteLine("新的線程已經啟動"); Console.WriteLine(B); // 全局靜態變量 } }
2.1.3 委托與Lambda
原理是 Thread 的構造函數 public Thread(ThreadStart start);
,ThreadStart
是一個委托,其定義如下
public delegate void ThreadStart();
使用委托的話,可以這樣寫
static void Main(string[] args) { System.Threading.ThreadStart start = DelegateThread; Thread thread = new Thread(start); thread.Name = "Test"; thread.Start(); Console.ReadKey(); } public static void DelegateThread() { OneTest("a", "b", 666, new Program()); } public static void OneTest(string a, string b, int c, Program p) { Console.WriteLine("新的線程已經啟動"); }
有那麼一點點麻煩,不過我們可以使用 Lambda 快速實現。
使用 Lambda 示例如下:
static void Main(string[] args) { Thread thread = new Thread(() => { OneTest("a", "b", 666, new Program()); }); thread.Name = "Test"; thread.Start(); Console.ReadKey(); } public static void OneTest(string a, string b, int c, Program p) { Console.WriteLine("新的線程已經啟動"); }
提示:如果需要處理的算法比較簡單的話,可以直接寫進委托中,不需要另外寫方法啦。
可以看到,C# 是多麼的方便。
2.2 暫停與阻塞
Thread.Sleep()
方法可以將當前線程掛起一段時間,Thread.Join()
方法可以阻塞當前線程一直等待另一個線程運行至結束。
在等待線程 Sleep()
或 Join()
的過程中,線程是阻塞的(Blocket)。
阻塞的定義:當線程由於特點原因暫停執行,那麼它就是阻塞的。
如果線程處於阻塞狀態,線程就會交出他的 CPU 時間片,並且不會消耗 CPU 時間,直至阻塞結束。
阻塞會發生上下文切換。
代碼示例如下:
static void Main(string[] args) { Thread thread = new Thread(OneTest); thread.Name = "小弟弟"; Console.WriteLine($"{DateTime.Now}:大傢在吃飯,吃完飯後要帶小弟弟逛街"); Console.WriteLine("吃完飯瞭"); Console.WriteLine($"{DateTime.Now}:小弟弟開始玩遊戲"); thread.Start(); // 化妝 5 s Console.WriteLine("不管他,大姐姐化妝先"); Thread.Sleep(TimeSpan.FromSeconds(5)); Console.WriteLine($"{DateTime.Now}:化完妝,等小弟弟打完遊戲"); thread.Join(); Console.WriteLine("打完遊戲瞭嘛?" + (!thread.IsAlive ? "true" : "false")); Console.WriteLine($"{DateTime.Now}:走,逛街去"); Console.ReadKey(); } public static void OneTest() { Console.WriteLine(Thread.CurrentThread.Name + "開始打遊戲"); for (int i = 0; i < 10; i++) { Console.WriteLine($"{DateTime.Now}:第幾局:" + i); Thread.Sleep(TimeSpan.FromSeconds(2)); // 休眠 2 秒 } Console.WriteLine(Thread.CurrentThread.Name + "打完瞭"); }
Join() 也可以實現簡單的線程同步,即一個線程等待另一個線程完成。
2.3 線程狀態
ThreadState
是一個枚舉,記錄瞭線程的狀態,我們可以從中判斷線程的生命周期和健康情況。
其枚舉如下:
枚舉 | 值 | 說明 |
---|---|---|
Initialized | 0 | 此狀態指示線程已初始化但尚未啟動。 |
Ready | 1 | 此狀態指示線程因無可用的處理器而等待使用處理器。 線程準備在下一個可用的處理器上運行。 |
Running | 2 | 此狀態指示線程當前正在使用處理器。 |
Standby | 3 | 此狀態指示線程將要使用處理器。 一次隻能有一個線程處於此狀態。 |
Terminated | 4 | 此狀態指示線程已完成執行並已退出。 |
Transition | 6 | 此狀態指示線程在可以執行前等待處理器之外的資源。 例如,它可能正在等待其執行堆棧從磁盤中分頁。 |
Unknown | 7 | 線程的狀態未知。 |
Wait | 5 | 此狀態指示線程尚未準備好使用處理器,因為它正在等待外圍操作完成或等待資源釋放。 當線程就緒後,將對其進行重排。 |
但是裡面有很多枚舉類型是沒有用處的,我們可以使用一個這樣的方法來獲取更加有用的信息:
public static ThreadState GetThreadState(ThreadState ts) { return ts & (ThreadState.Unstarted | ThreadState.WaitSleepJoin | ThreadState.Stopped); }
根據 2.2 中的示例,我們修改一下 Main 中的方法:
static void Main(string[] args) { Thread thread = new Thread(OneTest); thread.Name = "小弟弟"; Console.WriteLine($"{DateTime.Now}:大傢在吃飯,吃完飯後要帶小弟弟逛街"); Console.WriteLine("吃完飯瞭"); Console.WriteLine($"{DateTime.Now}:小弟弟開始玩遊戲"); Console.WriteLine("弟弟在幹嘛?(線程狀態):" + Enum.GetName(typeof(ThreadState), GetThreadState(thread.ThreadState))); thread.Start(); Console.WriteLine("弟弟在幹嘛?(線程狀態):" + Enum.GetName(typeof(ThreadState), GetThreadState(thread.ThreadState))); // 化妝 5 s Console.WriteLine("不管他,大姐姐化妝先"); Thread.Sleep(TimeSpan.FromSeconds(5)); Console.WriteLine("弟弟在幹嘛?(線程狀態):" + Enum.GetName(typeof(ThreadState), GetThreadState(thread.ThreadState))); Console.WriteLine($"{DateTime.Now}:化完妝,等小弟弟打完遊戲"); thread.Join(); Console.WriteLine("弟弟在幹嘛?(線程狀態):" + Enum.GetName(typeof(ThreadState), GetThreadState(thread.ThreadState))); Console.WriteLine("打完遊戲瞭嘛?" + (!thread.IsAlive ? "true" : "false")); Console.WriteLine($"{DateTime.Now}:走,逛街去"); Console.ReadKey(); }
代碼看著比較亂,請復制到項目中運行一下。
輸出示例:
2020/4/11 11:01:48:大傢在吃飯,吃完飯後要帶小弟弟逛街 吃完飯瞭 2020/4/11 11:01:48:小弟弟開始玩遊戲 弟弟在幹嘛?(線程狀態):Unstarted 弟弟在幹嘛?(線程狀態):Running 不管他,大姐姐化妝先 小弟弟開始打遊戲 2020/4/11 11:01:48:第幾局:0 2020/4/11 11:01:50:第幾局:1 2020/4/11 11:01:52:第幾局:2 弟弟在幹嘛?(線程狀態):WaitSleepJoin 2020/4/11 11:01:53:化完妝,等小弟弟打完遊戲 2020/4/11 11:01:54:第幾局:3 2020/4/11 11:01:56:第幾局:4 2020/4/11 11:01:58:第幾局:5 2020/4/11 11:02:00:第幾局:6 2020/4/11 11:02:02:第幾局:7 2020/4/11 11:02:04:第幾局:8 2020/4/11 11:02:06:第幾局:9 小弟弟打完瞭 弟弟在幹嘛?(線程狀態):Stopped 打完遊戲瞭嘛?true 2020/4/11 11:02:08:走,逛街去
可以看到 Unstarted
、WaitSleepJoin
、Running
、Stopped
四種狀態,即未開始(就緒)、阻塞、運行中、死亡。
2.4 終止
.Abort()
方法不能在 .NET Core 上使用,不然會出現 System.PlatformNotSupportedException:“Thread abort is not supported on this platform.”
。
後面關於異步的文章會講解如何實現終止。
由於 .NET Core 不支持,就不理會這兩個方法瞭。這裡隻列出 API,不做示例。
方法 | 說明 |
---|---|
Abort() | 在調用此方法的線程上引發 ThreadAbortException,以開始終止此線程的過程。 調用此方法通常會終止線程。 |
Abort(Object) | 引發在其上調用的線程中的 ThreadAbortException以開始處理終止線程,同時提供有關線程終止的異常信息。 調用此方法通常會終止線程。 |
Abort()
方法給線程註入 ThreadAbortException
異常,導致程序被終止。但是不一定可以終止線程。
2.5 線程的不確定性
線程的不確定性是指幾個並行運行的線程,不確定在下一刻 CPU 時間片會分配給誰(當然,分配有優先級)。
對我們來說,多線程是同時運行
的,但一般 CPU 沒有那麼多核,不可能在同一時刻執行所有的線程。CPU 會決定某個時刻將時間片分配給多個線程中的一個線程,這就出現瞭 CPU 的時間片分配調度。
執行下面的代碼示例,你可以看到,兩個線程打印的順序是不確定的,而且每次運行結果都不同。
CPU 有一套公式確定下一次時間片分配給誰,但是比較復雜,需要學習計算機組成原理和操作系統。
留著下次寫文章再講。
static void Main(string[] args) { Thread thread1 = new Thread(Test1); Thread thread2 = new Thread(Test2); thread1.Start(); thread2.Start(); Console.ReadKey(); } public static void Test1() { for (int i = 0; i < 10; i++) { Console.WriteLine("Test1:" + i); } } public static void Test2() { for (int i = 0; i < 10; i++) { Console.WriteLine("Test2:" + i); } }
2.6 線程優先級、前臺線程和後臺線程
Thread.Priority
屬性用於設置線程的優先級,Priority
是一個 ThreadPriority 枚舉,其枚舉類型如下
枚舉 | 值 | 說明 |
---|---|---|
AboveNormal | 3 | 可以將 安排在具有 Highest 優先級的線程之後,在具有 Normal 優先級的線程之前。 |
BelowNormal | 1 | 可以將 Thread 安排在具有 Normal 優先級的線程之後,在具有 Lowest 優先級的線程之前。 |
Highest | 4 | 可以將 Thread 安排在具有任何其他優先級的線程之前。 |
Lowest | 0 | 可以將 Thread 安排在具有任何其他優先級的線程之後。 |
Normal | 2 | 可以將 Thread 安排在具有 AboveNormal 優先級的線程之後,在具有 BelowNormal 優先級的線程之前。 默認情況下,線程具有 Normal 優先級。 |
優先級排序:Highest
> AboveNormal
> Normal
> BelowNormal
> Lowest
。
Thread.IsBackgroundThread
可以設置線程是否為後臺線程。
前臺線程的優先級大於後臺線程,並且程序需要等待所有前臺線程執行完畢後才能關閉;而當程序關閉是,無論後臺線程是否在執行,都會強制退出。
2.7 自旋和休眠
當線程處於進入休眠狀態或解除休眠狀態時,會發生上下文切換,這就帶來瞭昂貴的消耗。
而線程不斷運行,就會消耗 CPU 時間,占用 CPU 資源。
對於過短的等待,應該使用自旋(spin)方法,避免發生上下文切換;過長的等待應該使線程休眠,避免占用大量 CPU 時間。
我們可以使用最為熟知的 Sleep()
方法休眠線程。有很多同步線程的類型,也使用瞭休眠手段等待線程(已經寫好草稿啦)。
自旋的意思是,沒事找事做。
例如:
public static void Test(int n) { int num = 0; for (int i=0;i<n;i++) { num += 1; } }
通過做一些簡單的運算,來消耗時間,從而達到等待的目的。
C# 中有關於自旋的自旋鎖和 Thread.SpinWait();
方法,在後面的線程同步分類中會說到自旋鎖。
Thread.SpinWait()
在極少數情況下,避免線程使用上下文切換很有用。其定義如下
public static void SpinWait(int iterations);
SpinWait 實質上是(處理器)使用瞭非常緊密的循環,並使用 iterations
參數指定的循環計數。 SpinWait 等待時間取決於處理器的速度。
SpinWait 無法使你準確控制等待時間,主要是使用一些鎖時用到,例如 Monitor.Enter。
到此這篇關於C#多線程系列之線程的創建和生命周期的文章就介紹到這瞭。希望對大傢的學習有所幫助,也希望大傢多多支持WalkonNet。