这篇文章将在上篇文章的基础上,进一步讲解线程的相关知识。这篇文章涉及到的知识点有 线程优先级、前台与后台线程、线程参数、lock、Monitor 和 线程异常处理 。这篇要比上一篇难度有一点提高,但是大家不用担心,我依然会用大量的代码来展示每个知识点,并且对于其中的难点我会详细讲解。下面我们就开始学习基础知识的第二篇。
一、线程优先级
.NET 给我们定义了多种线程优先级,它们都位于 ThreadPriority 中,分别是: Lowest 、BelowNormal 、 Normal 、 AboveNormal 和 Highest 。它们的优先级和说明如下表所示:
在普通的情况下,如果优先级高的线程在运行,就不会给优先级低的线程分配任何 CPU 时间,这样就可以保证重要/主要线程具有较高的优先级。在大多数的时间内,这个线程什么也不做,而其他线程则执行它们的任务。一旦有信息输入,这个线程就立即获得比其他线程更高的优先级,在短时间内处理输入的信息。根据我在项目中的经验来看,高优先级的线程一般用在 处理用户输入数据、重要数据处理 和 应用程序主线程 。下面我们通过一个例子来看一下线程的优先级。
using System.Threading; using static System.Console; namespace Thread_Priority { class Program { static int printNumberRunCount = 0; static int printStringRunCount = 0; static void Main(string[] args) { Process.GetCurrentProcess().ProcessorAffinity = new System.IntPtr(1); Thread printNumberThread = new Thread(PrintNumber); Thread printStringThread = new Thread(PrintString); printNumberThread.Priority = ThreadPriority.Highest; printStringThread.Priority = ThreadPriority.Lowest; printNumberThread.Start(); printStringThread.Start(); Thread.Sleep(2000); printNumberThread.Abort(); printStringThread.Abort(); Write($"PrintNumber 循环了:{printNumberRunCount} 次,printStringThread 循环了:{printStringRunCount} 次"); Read(); } static void PrintNumber() { for (int i = 0; i < 10000; i++) { printNumberRunCount++; WriteLine($"输出数字:{i}"); } } static void PrintString() { for(int i=0;i<10000;i++) { printStringRunCount++; WriteLine("are you ok?"); } } } }
在上面的代码中我们创建了两个线程 printNumberThread 和 printStringThread 这两个线程分别调用 PrintNumber 和 PrintString 方法。并且我们也定义了两个统计线程运行次数的变量 printNumberRunCount 和 printStringRunCount。之后我们将 printNumberThread 线程的优先级设置为最高 Highest ,将 printStringThread 线程的优先级设置为最低 Lowest ,接着我们在运行线程两秒后将线程停掉,这时我们从控制台输出的内容可以看出来高优先级的线程 printNumberThread 循环的次数大于低优先级的线程 printStringThread 循环的次数。 在代码中有这么一行 Process.GetCurrentProcess().ProcessorAffinity = new System.IntPtr(1);
这段代码的意思是告诉操作系统将所有的线程都在 CPU 的第一个核心上进行运算。加这么一段代码只是为了将优先级更明显的表现出来而已,实际开发中除非特殊情况,一般不这么写。
Tip:
优先级越高所占用的 CPU 时间就会越多。但是即使我们手动设置最高的优先级,也不会超过操作系统进程的优先级。
二、前台与后台线程
前台线程和后台线程大体上是一样的,唯一的不同是进程中所有的前台线程都完成工作完后,就会马上结束进程工作,即使还有后台线程在工作。简单的说就是后台线程不会确保进程一直运行,当进程中的所有前台线程都停止,系统会关闭所有后台线程。我们可以通过 Thread 的 IsBackground 属性来设置线程是前台线程还是后台线程,当复制为 True 时表示时后台线程,繁反之为前台线程。这里需要注意的是属于线程池的线程是后台线程,从非托管代码进入托管执行环境的线程都会变为后台线程,默认情况下通过新建并启动 Thread 对象生成的所有线程都是前台线程。
using System.Threading; using static System.IO.File; using static System.Console; namespace ForegroundBackgroundThread { class Program { static void Main(string[] args) { Thread printNumberThread = new Thread(PrintNumber); Thread printStringThread = new Thread(PrintString); printStringThread.IsBackground = true; printNumberThread.Start(); printStringThread.Start(); } static void PrintNumber() { for (int i = 0; i < 20; i++) { AppendAllText("PrintNumber.txt",i+"\r\n"); } } static void PrintString() { for(int i=0;i<50;i++) { AppendAllText("PrintString.txt", i + "\r\n"); } } } }
上述代码中我们定义了两个线程 printNumberThread 和 printStringThread ,并分别调用 PrintNumber 和 PrintString 方法。其中 PrintNumber 方法我们循环输出 20 个数字,PrintString 方法我们循环输出 50 个数字。然后我们将 printStringThread 线程通过属性 IsBackground 设置为后台线程,最后启动这两个线程。在代码运行完毕后我们来查看一下输出的两个文件 PrintNumber.txt 和 PrintString.txt 中的内容。
从两个文件的内容可以看出,PrintNumber 文件输出了所有的数字,而 PrintString 却没有输出所有数字,这是因为 printStringThread 为后台线程,当 printNumberThread 线程执行完毕后进程就推出了。
那么到这里会有很多读者要问了,前后台线程有什么用呢?后台线程适用于后台任务,例如将被动侦听活动的线程设置为后台线程,将负责发送数据的线程设置为前台线程,这样在所有的数据发送完毕之后台前线程不会被终止。前台线程用于需要长时间等待的任务,例如监听客户端请求。后台线程用于处理时间较短的任务,例如处理客户端发送的请求。
三、线程参数
前面我们创建线程调用的方法都是不带参数的,但是在实际开发中线程调用的方法不带参数的情况很少,大部分情况都是带有参数的,那么遇到这种情况我们该怎么处理呢?我先来看一下代码。
using System.Threading; using static System.Console; namespace ThreadPara { class Program { static void Main(string[] args) { Thread thread = new Thread(()=>PrintNumber(50)); thread.Start(); Read(); } static void PrintNumber(int number) { for (int i = 0; i < number; i++) { WriteLine($"输出数字:{i}"); } } } }
上述代码中我们利用匿名方法调用 PrintNumber 方法,并将参数传递进来。除了这种方法还有另一种方式传递参数,通过 Thread.Start(para) 传递参数,代码如下:
using System.Threading; using static System.Console; namespace ThreadPara { class Program { static void Main(string[] args) { Thread thread = new Thread(PrintNumber); thread.Start(50); Read(); } static void PrintNumber(object number) { for (int i = 0; i < (int)number; i++) { WriteLine($"输出数字:{i}"); } } } }