在上一篇多線程(基礎篇1)中,我們主要講述了如何創建線程、中止線程、線程等待以及終止線程的相關知識,在本篇中我們繼續講述有關線程的一些知識。
五、確定線程的狀態
在這一節中,我們將講述如何查看一個線程的狀態,通常知道一個線程處於什麼狀態是非常有用的。但是,要注意線程是獨立運行的,它的狀態可能隨時變化。要查看一個線程的狀態,我們可以按如下步驟進行:
1、使用Visual Studio 2015創建一個新的控制台應用程序。
2、雙擊打開“Program.cs”文件,然後修改為如下代碼:
1 using System;
2 using System.Threading;
3 using static System.Console;
4 using static System.Threading.Thread;
5
6 namespace Recipe05
7 {
8 class Program
9 {
10 static void DoNothing()
11 {
12 Sleep(TimeSpan.FromSeconds(2));
13 }
14
15 static void PrintNumbersWithStatus()
16 {
17 WriteLine("Starting...");
18 WriteLine(CurrentThread.ThreadState.ToString());
19
20 for(int i = 1; i < 10; i++)
21 {
22 Sleep(TimeSpan.FromSeconds(2));
23 WriteLine(i);
24 }
25 }
26
27 static void Main(string[] args)
28 {
29 WriteLine("Starting program...");
30
31 Thread t1 = new Thread(PrintNumbersWithStatus);
32 Thread t2 = new Thread(DoNothing);
33
34 WriteLine(t1.ThreadState.ToString());
35
36 t2.Start();
37 t1.Start();
38
39 for(int i = 1; i < 30; i++)
40 {
41 WriteLine(i.ToString() + " - " + t1.ThreadState.ToString());
42 }
43
44 Sleep(TimeSpan.FromSeconds(6));
45
46 t1.Abort();
47
48 WriteLine("A thread has been aborted");
49 WriteLine(t1.ThreadState.ToString());
50 WriteLine(t2.ThreadState.ToString());
51 }
52 }
53 }
3、運行該控制台應用程序,運行效果(每次運行效果可能不同)如下圖所示:

在上述代碼中,我們在“Main”方法中定義了兩個不同的線程t1和t2,t1線程在執行過程中被終止,t2線程成功執行完畢。我們可以使用Thread對象的ThreadState屬性獲得某個線程的狀態信息,ThreadState屬性是C#枚舉類型。
當程序執行到第34行代碼處時,t1線程還沒有執行,這個時候t1線程的狀態為“ThreadState.Unstarted”。
當程序執行到第37行代碼處時,t1線程開始執行,這個時候該線程的狀態為“ThreadState.Running”。
當運行到第39~42行代碼處時,主線程開始執行循環語句,持續打印出29條t1線程的狀態,因為在“PrintNumbersWithStatus”方法中調用了“Thread.Sleep”方法,因此在執行主線程的循環的過程中,t1線程的狀態在“ThreadState.Running”和“ThreadState.WaitSleepJoin”之間轉換。
當程序執行到第44行代碼處時,在主線程中調用了“Thread.Sleep”方法,在等待6秒期間,“PrintNumbersWithStatus”方法中的for循環代碼不斷執行,因此打印出1、2、3共三行數字。
當程序執行到第46行代碼處時,我們調用了t1的“Abort”方法,從而t1線程被終止。
當程序執行到第49行代碼處時,由於已經在t1線程上調用了“Abort”方法,因此,它的狀態為“ThreadState.Aborted”或“ThreadState.AbortRequested”。
當程序執行到第50行代碼處時,因為t2線程正常執行完畢,因此t2線程的狀態為“ThreadState.Stopped”。
六、線程優先級
在這一小節中,我們將講述線程的優先級,線程的優先級確定了線程所能使用CPU時間的大小。我們按以下步驟來完成線程優先級的學習:
1、使用Visual Studio 2015創建一個新的控制台應用程序。
2、雙擊打開“Program.cs”文件,修改代碼如下所示:
1 using System;
2 using System.Threading;
3 using static System.Console;
4 using static System.Threading.Thread;
5 using static System.Diagnostics.Process;
6
7 namespace Recipe06
8 {
9 class ThreadSample
10 {
11 private bool isStopped = false;
12
13 public void Stop()
14 {
15 isStopped = true;
16 }
17
18 public void CountNumbers()
19 {
20 long counter = 0;
21 while (!isStopped)
22 {
23 counter++;
24 }
25
26 WriteLine($"{CurrentThread.Name} with {CurrentThread.Priority,10} priority has a count = {counter,15:N0}");
27 }
28 }
29
30 class Program
31 {
32 static void RunThreads()
33 {
34 var sample = new ThreadSample();
35
36 var threadOne = new Thread(sample.CountNumbers);
37 threadOne.Name = "ThreadOne";
38
39 var threadTwo = new Thread(sample.CountNumbers);
40 threadTwo.Name = "ThreadTwo";
41
42 threadOne.Priority = ThreadPriority.Highest;
43 threadTwo.Priority = ThreadPriority.Lowest;
44
45 threadOne.Start();
46 threadTwo.Start();
47
48 Sleep(TimeSpan.FromSeconds(2));
49 sample.Stop();
50 }
51
52 static void Main(string[] args)
53 {
54 WriteLine($"Current thread priority: {CurrentThread.Priority}");
55 WriteLine("Running on all cores available");
56
57 RunThreads();
58
59 Sleep(TimeSpan.FromSeconds(2));
60
61 WriteLine("Running on a single core");
62 GetCurrentProcess().ProcessorAffinity = new IntPtr(1);
63 RunThreads();
64 }
65 }
66 }
3、運行該控制台應用程序,運行效果(每次運行效果可能不同)如下圖所示:

在上述代碼中,我們定義了兩個不同的線程threadOne和threadTwo。threadOne線程具有最高的優先級“ThreadPriority.Highest”,threadTwo具有最低的優先級“ThreadPriority.Lowest”。
在程序執行到第57行代碼處,如果我們的計算機是多核計算機,我們將在2秒內獲得初始結果,並且具有最高優先級的threadOne線程要比具有最低優先級的threadTwo線程的迭代次數更多一些,但也不會相差太遠。但是,如果我們的計算機是單核計算機,則結果大不相同。
為了模擬單核計算機,我們將ProcessorAffinity的值設置為1,現在這兩個線程所迭代的次數將差異非常大,並且程序的執行時間遠遠大於2秒。這是因為CPU的計算機時間大部分給予了高優先級的線程,而低優先級的線程獲得非常少的CPU時間。
七、前台線程和後台線程
在這一小節中,我們將講述什麼是前台線程和後台線程,並且講述如何設置這個選項以影響程序的行為。我們按以下步驟來完成前台線程和後台線程的學習:
1、使用Visual Studio 2015創建一個新的控制台應用程序。
2、雙擊打開“Program.cs”文件,修改代碼如下所示:
1 using System;
2 using System.Threading;
3 using static System.Console;
4 using static System.Threading.Thread;
5
6 namespace Recipe07
7 {
8 class ThreadSample
9 {
10 private readonly int iterations;
11
12 public ThreadSample(int iterations)
13 {
14 this.iterations = iterations;
15 }
16
17 public void CountNumbers()
18 {
19 for(int i = 0; i < iterations; i++)
20 {
21 Sleep(TimeSpan.FromSeconds(0.5));
22 WriteLine($"{CurrentThread.Name} prints {i}");
23 }
24 }
25 }
26
27 class Program
28 {
29 static void Main(string[] args)
30 {
31 var sampleForeground = new ThreadSample(10);
32 var sampleBackground = new ThreadSample(20);
33
34 var threadOne = new Thread(sampleForeground.CountNumbers);
35 threadOne.Name = "ForegroundThread";
36
37 var threadTwo = new Thread(sampleBackground.CountNumbers);
38 threadTwo.Name = "BackgroundThread";
39 threadTwo.IsBackground = true;
40
41 threadOne.Start();
42 threadTwo.Start();
43 }
44 }
45 }
3、運行該控制台應用程序,運行效果(每次運行效果可能不同)如下圖所示:

在上述代碼中,我們定義了兩個不同的線程threadOne和threadTwo。當我們創建一個線程時,該線程顯示地是一個前台線程,比如threadOne。如果我們要將一個線程設置為後台線程,只需要將該線程的“IsBackground”屬性設置為“true”即可,比如threadTwo。我們還配置了兩個線程執行的循環次數不一樣,threadOne線程所執行的循環次數為10次,threadTwo線程所執行的循環次數為20次,因此threadOne線程要比threadTwo線程早執行完畢。
從執行結果上來看,當threadOne線程執行完畢後,主程序也結束了,並且後台線程也被終止了,這就是前台線程和後台線程的區別:進程會等待所有的前台進程執行完畢後才結束,單不會等待後台進程執行完畢。
八、向線程傳遞參數
在這一小節,我將講述如何向一個線程傳遞參數,我們將使用不同的方法來完成這個工作,具體步驟如下所示:
1、使用Visual Studio 2015創建一個新的控制台應用程序。
2、雙擊打開“Program.cs”文件,修改代碼如下所示:
1 using System;
2 using System.Threading;
3 using static System.Console;
4 using static System.Threading.Thread;
5
6 namespace Recipe08
7 {
8 class ThreadSample
9 {
10 private readonly int iterations;
11
12 public ThreadSample(int iterations)
13 {
14 this.iterations = iterations;
15 }
16
17 public void CountNumbers()
18 {
19 for(int i = 1; i <= iterations; i++)
20 {
21 Sleep(TimeSpan.FromSeconds(0.5));
22 WriteLine($"{CurrentThread.Name} prints {i}");
23 }
24 }
25 }
26
27 class Program
28 {
29 static void Count(object iterations)
30 {
31 CountNumbers((int)iterations);
32 }
33
34 static void CountNumbers(int iterations)
35 {
36 for(int i = 1; i <= iterations; i++)
37 {
38 Sleep(TimeSpan.FromSeconds(0.5));
39 WriteLine($"{CurrentThread.Name} prints {i}");
40 }
41 }
42
43 static void PrintNumber(int number)
44 {
45 WriteLine(number);
46 }
47
48 static void Main(string[] args)
49 {
50 var sample = new ThreadSample(10);
51
52 var threadOne = new Thread(sample.CountNumbers);
53 threadOne.Name = "ThreadOne";
54 threadOne.Start();
55 threadOne.Join();
56
57 WriteLine("--------------------------");
58
59 var threadTwo = new Thread(Count);
60 threadTwo.Name = "ThreadTwo";
61 threadTwo.Start(8);
62 threadTwo.Join();
63
64 WriteLine("--------------------------");
65
66 var threadThree = new Thread(() => CountNumbers(12));
67 threadThree.Name = "ThreadThree";
68 threadThree.Start();
69 threadThree.Join();
70
71 WriteLine("--------------------------");
72
73 int i = 10;
74 var threadFour = new Thread(() => PrintNumber(i));
75
76 i = 20;
77 var threadFive = new Thread(() => PrintNumber(i));
78
79 threadFour.Start();
80 threadFive.Start();
81 }
82 }
83 }
3、運行該控制台應用程序,運行效果如下圖所示:

在第50~55行代碼處,我們首先創建了一個ThreadSample類型的對象,並將數字10傳遞給了該類型的構造方法。然後我們將ThreadSample對象的“CountNumbers”方法傳遞給了線程threadOne的構造方法,“CountNumbers”方法將在線程threadOne中被執行,因此我們通過“CountNumbers”方法將數字10傳遞給了線程threadOne。
在第59~62行代碼處,我們使用“Thread.Start”的重載方法將一個對象傳遞給線程threadTwo,要使該段代碼正確運行,我們要保證在構造threadTwo線程時,傳給給Thread的構造方法的委托要帶有一個object類型的參數。在這段代碼中,我們將8看作一個對象傳遞給“Count”方法,該方法有調用了同名的重載方法將object對象轉換為整數類型。
在第66~69行代碼處,我們在創建threadThree線程時,給Thread構造方法傳遞的是一個lambda表達式,該表達式定義了一個不屬於任何類的方法,我們在該lambda表達式中調用了“CountNumbers”方法,並將12傳遞給了“CountNumbers”方法,通過lambda表達式,我們將12傳遞給了threadThree線程。
注意:當我們將一個局部變量用於多個lambda表達式時,這些lambda表達式將會共享這個變量的值,在第73~80行代碼處,我們將局部變量用於了兩個lambda表達式,我們運行threadFour和threadFive線程後,我們發現打印出來的結果都是20。
未完待續!