在上一篇C#多线程之基础篇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。
未完待续!