目錄
- 1,擷取目前線程資訊
- 2,管理線程狀态
- 2.1 啟動與參數傳遞
- 2.1.1 ParameterizedThreadStart
- 2.1.2 使用靜态變量或類成員變量
- 2.1.3 委托與Lambda
- 2.2 暫停與阻塞
- 2.3 線程狀态
- 2.4 終止
- 2.5 線程的不确定性
- 2.6 線程優先級、前台線程和背景線程
- 2.7 自旋和休眠
- 2.1 啟動與參數傳遞
本篇是《多線程入門和實踐(初級)》的第一篇,也是大家相當熟悉和不屑的的最簡單的入門部分。作為系列文章,筆者将從最簡單的部分開始,與各位夥伴一起不斷學習和探究 C# 中的多線程。
對于涉及理論的東西,這裡不會過多讨論。更加深入的成分會在中級系列加以說明和探讨,屆時會有很多與底層相關的知識。
系列文章一般開頭都要寫一些寄語吧?
那我祝願各位同學要好好學習,天天向上。
學習多線程的第一步,就是學習 Thread。Thread 類可以建立和控制線程,設定其優先級并擷取其狀态。這一篇将開始學習線程的建立和生命周期。
官方文檔 Thread 類詳細的屬性和方法:
https://docs.microsoft.com/zh-cn/dotnet/api/system.threading.thread?view=netcore-3.1#properties
來,打開你的
Visual Studio
,一起撸代碼。
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
一般認為,線程有五種狀态:
建立(new 對象) 、就緒(等待CPU排程)、運作(CPU正在運作)、阻塞(等待阻塞、同步阻塞等)、死亡(對象釋放)。
理論的東西不說太多,直接撸代碼。
建立線程簡直滾瓜爛熟,無非
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);
我們以啟動新的線程時傳遞參數來舉例,使用這四個構造函數呢?
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);
}
此種方法不需要作為參數傳遞,各個線程共享堆棧。
優點是不需要裝箱拆箱,多線程可以共享空間;缺點是變量是大家都可以通路,此種方式在多線程競價時,可能會導緻多種問題(可以加鎖解決)。
下面使用兩個變量實作資料傳遞:
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); // 全局靜态變量
}
}
原理是 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# 是多麼的友善。
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() 也可以實作簡單的線程同步,即一個線程等待另一個線程完成。
ThreadState
是一個枚舉,記錄了線程的狀态,我們可以從中判斷線程的生命周期和健康情況。
其枚舉如下:
枚舉 | 值 | 說明 |
---|---|---|
Initialized | 此狀态訓示線程已初始化但尚未啟動。 | |
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);
}
此方法來自:《C# 7.0 核心技術指南》第十四章。
根據 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
四種狀态,即未開始(就緒)、阻塞、運作中、死亡。
.Abort()
方法不能在 .NET Core 上使用,不然會出現
System.PlatformNotSupportedException:“Thread abort is not supported on this platform.”
後面關于異步的文章會講解如何實作終止。
由于 .NET Core 不支援,就不理會這兩個方法了。這裡隻列出 API,不做示例。
方法 | |
---|---|
Abort() | 在調用此方法的線程上引發 ThreadAbortException,以開始終止此線程的過程。 調用此方法通常會終止線程。 |
Abort(Object) | 引發在其上調用的線程中的 ThreadAbortException以開始處理終止線程,同時提供有關線程終止的異常資訊。 調用此方法通常會終止線程。 |
Abort()
方法給線程注入
ThreadAbortException
異常,導緻程式被終止。但是不一定可以終止線程。
線程的不确定性是指幾個并行運作的線程,不确定在下一刻 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);
}
}
Thread.Priority
屬性用于設定線程的優先級,
Priority
是一個 ThreadPriority 枚舉,其枚舉類型如下
AboveNormal | 可以将 安排在具有 優先級的線程之後,在具有 優先級的線程之前。 | |
BelowNormal | 可以将 Thread 安排在具有 | |
Highest | 可以将 Thread 安排在具有任何其他優先級的線程之前。 | |
Lowest | 可以将 Thread 安排在具有任何其他優先級的線程之後。 | |
Normal | 優先級的線程之前。 預設情況下,線程具有 優先級。 |
優先級排序:
Highest
>
AboveNormal
Normal
BelowNormal
Lowest
Thread.IsBackgroundThread
可以設定線程是否為背景線程。
前台線程的優先級大于背景線程,并且程式需要等待所有前台線程執行完畢後才能關閉;而當程式關閉是,無論背景線程是否在執行,都會強制退出。
當線程處于進入休眠狀态或解除休眠狀态時,會發生上下文切換,這就帶來了昂貴的消耗。
而線程不斷運作,就會消耗 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。
一個逗逗的大學生