C#多執行緒系列(1):Thread
本篇是《多執行緒入門和實踐(初級)》的第一篇,也是大家相當熟悉和不屑的的最簡單的入門部分。作為系列文章,筆者將從最簡單的部分開始,與各位夥伴一起不斷學習和探究 C# 中的多執行緒。
對於涉及理論的東西,這裡不會過多討論。更加深入的成分會在中級系列加以說明和探討,屆時會有很多與底層相關的知識。
系列文章一般開頭都要寫一些寄語吧?
那我祝願各位同學要好好學習,天天向上。
學習多執行緒的第一步,就是學習 Thread。Thread 類可以創建和控制執行緒,設置其優先順序並獲取其狀態。這一篇將開始學習執行緒的創建和生命周期。
官方文檔 Thread 類詳細的屬性和方法:
//docs.microsoft.com/zh-cn/dotnet/api/system.threading.thread?view=netcore-3.1#properties
來,打開你的 Visual Studio
,一起擼程式碼。
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 + "打完了");
}
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 等待時間取決於處理器的速度。