二、.NET中的多线程编程
2.1 如何在.NET程序中手动控制多个线程?
最直接且灵活性最大的,莫过于主动创建、运行、结束所有线程。
(1)第一个多线程程序
.NET提供了非常直接的控制线程类型的类型:System.Threading.Thread类。下面是一个简单的多线程程序:
class Program { static void Main(string[] args) { Console.WriteLine("进入多线程工作模式:"); for (int i = 0; i < 10; i++) { Thread newThread = new Thread(Work); // 开启新线程 newThread.Start(); } Console.ReadKey(); } static void Work() { Console.WriteLine("线程开始"); // 模拟做了一些工作,耗费1s时间 Thread.Sleep(1000); Console.WriteLine("线程结束"); } }
在主线程中,该代码创建了10个新的线程,这个10个线程的工作互不干扰,宏观上来看它们应该是并行运行的,执行的结果也证实了这一点:
PS:当new了一个Thread类型对象并不意味着生成了一个线程,线程的生成是在调用Thread的Start方法的时候。另外这里的线程并不一定是操作系统层面上产生的一个真正线程!
(2)控制线程的状态
在任意时刻,.NET中的线程都会处于如下图所示的几个状态中的某一个状态上,该图也直观地展示了一个线程可能经过的状态转换过程
下面的示例代码则展示了我们如何手动地查看和控制一个线程的状态:
class Program { static void Main(string[] args) { Console.WriteLine("开始测试线程1"); // 初始化一个线程 thread1 Thread thread1 = new Thread(Work1); // 这时状态:UnStarted PrintState(thread1); // 启动线程 Console.WriteLine("现在启动线程"); thread1.Start(); // 这时状态:Running PrintState(thread1); // 让线程飞一会 3s Thread.Sleep(3 * 1000); // 让线程挂起 Console.WriteLine("现在挂起线程"); thread1.Suspend(); // 给线程足够的时间来挂起,否则状态可能是SuspendRequested Thread.Sleep(1000); // 这时状态:Suspend PrintState(thread1); // 继续线程 Console.WriteLine("现在继续线程"); thread1.Resume(); // 这时状态:Running PrintState(thread1); // 停止线程 Console.WriteLine("现在停止线程"); thread1.Abort(); // 给线程足够的时间来终止,否则的话可能是AbortRequested Thread.Sleep(1000); // 这时状态:Stopped PrintState(thread1); Console.WriteLine("------------------------------"); Console.WriteLine("开始测试线程2"); // 初始化一个线程 thread2 Thread thread2 = new Thread(Work2); // 这时状态:UnStarted PrintState(thread2); // 启动线程 thread2.Start(); Thread.Sleep(2 * 1000); // 这时状态:WaitSleepJoin PrintState(thread2); // 给线程足够的时间结束 Thread.Sleep(10 * 1000); // 这时状态:Stopped PrintState(thread2); Console.ReadKey(); } // 普通线程方法:一直在运行从未被超越 private static void Work1() { Console.WriteLine("线程运行中..."); // 模拟线程运行,但不改变线程状态 // 采用忙等状态 while (true) { } } // 文艺线程方法:运行10s就结束 private static void Work2() { Console.WriteLine("线程开始睡眠:"); // 睡眠10s Thread.Sleep(10 * 1000); Console.WriteLine("线程恢复运行"); } // 打印线程的状态 private static void PrintState(Thread thread) { Console.WriteLine("线程的状态是:{0}", thread.ThreadState.ToString()); } }
上述代码的执行结果如下图所示:
PS:在.NET Framework 4.0 及之后的版本中,已不鼓励使用线程的挂起状态及Suspend和Resume方法了。
2.2 如何使用.NET中的线程池?
(1).NET中的线程池是神马
线程的创建和销毁需要很大的性能开销,在Windows NT内核的操作系统中,每个进程都会包含一个线程池。
而在.NET中呢,也有自己的线程池,它是由CLR负责管理的。
线程池相当于一个缓存的概念,在该池中已经存在了一些没有被销毁的线程,而当应用程序需要一个新的线程时,就可以从线程池中直接获取一个已经存在的线程。相对应的,当一个线程被使用完毕后并不会立刻被销毁,而是放入线程池中等待下一次使用。
PS:线程池中运行的线程均为后台线程(即线程的 IsBackground 属性被设为true),所谓的后台线程是指这些线程的运行不会阻碍应用程序的结束。相反的,应用程序的结束则必须等待所有前台线程结束后才能退出。
(2)在.NET中使用线程池
在.NET中通过 System.Threading.ThreadPool 类型来提供关于线程池的操作,ThreadPool 类型提供了几个静态方法允许使用者插入一个工作线程的需求。常用的有以下三个静态方法:
① static bool QueueUserWorkItem(WaitCallback callback)
② static bool QueueUserWorkItem(WaitCallback callback, Object state)
③ static bool UnsafeQueueUserWorkItem(WaitCallback callback, Object state)
有了这几个方法,我们只需要将线程要处理的方法作为参数传入上述方法即可,随后的工作都由CLR的线程池管理程序来完成。其中,WaitCallback 是一个委托类型,该委托方法接受一个Object类型的参数且没有返回值。下面的代码展示了如何使用线程池来编写多线程的程序:
class Program { static void Main(string[] args) { string taskInfo = "运行10秒"; // 插入一个新的请求到线程池 bool result = ThreadPool.QueueUserWorkItem(DoWork, taskInfo); // 分配线程有可能会失败 if (!result) { Console.WriteLine("分配线程失败"); } else { Console.WriteLine("按回车键结束程序"); } Console.ReadKey(); } private static void DoWork(object state) { // 模拟做了一些操作,耗时10s for (int i = 0; i < 10; i++) { Console.WriteLine("工作者线程的任务是:{0}", state); Thread.Sleep(1000); } } }
上述代码执行后,如果不输入任何字符,那么会得到如下图所示的执行结果:
PS:事实上,UnsafeQueueWorkItem方法实现了完全相同的功能,二者的差别在于UnsafeQueueWorkItem方法不会将调用线程的堆栈传递给辅助线程,这就意味着主线程的权限限制不会传递给辅助线程。UnsafeQueueWorkItem由于不进行这样的传递,因此会得到更高的运行效率,但是潜在地提升了辅助线程的权限,也就有可能会成为一个潜在的安全漏洞。
2.3 如何查看和设置线程池的上下限?
通常情况下,我们无需修改默认的配置。但在一些场合,我们可能需要了解线程池的上下限和剩余的线程数。
线程池作为一个缓冲池,有着其上下限。在通常情况下,当线程池中的线程数小于线程池设置的下限时,线程池会设法创建新的线程,而当线程池中的线程数大于线程池设置的上限时,线程池将销毁多余的线程。
PS:在.NET Framework 4.0中,每个CPU默认的工作者线程数量最大值为250个,最小值为2个。而IO线程的默认最大值为1000个,最小值为2个。
在.NET中,通过 ThreadPool 类型提供的5个静态方法可以获取和设置线程池的上限和下限,同时它还额外地提供了一个方法来让程序员获知当前可用的线程数量,下面是这五个方法的签名:
① static void GetMaxThreads(out int workerThreads, out int completionPortThreads)
② static void GetMinThreads(out int workerThreads, out int completionPortThreads)
③ static bool SetMaxThreads(int workerThreads, int completionPortThreads)
④ static bool SetMinThreads(int workerThreads, int completionPortThreads)
⑤ static void GetAvailableThreads(out int workerThreads, out int completionPortThreads)
下面的代码示例演示了如何查询线程池的上下限阈值和可用线程数量:
class Program { static void Main(string[] args) { // 打印阈值和可用数量 GetLimitation(); GetAvailable(); // 使用掉其中三个线程 Console.WriteLine("此处申请使用3个线程..."); ThreadPool.QueueUserWorkItem(Work); ThreadPool.QueueUserWorkItem(Work); ThreadPool.QueueUserWorkItem(Work); Thread.Sleep(1000); // 打印阈值和可用数量 GetLimitation(); GetAvailable(); // 设置最小值 Console.WriteLine("此处修改了线程池的最小线程数量"); ThreadPool.SetMinThreads(10, 10); // 打印阈值 GetLimitation(); Console.ReadKey(); } // 运行10s的方法 private static void Work(object o) { Thread.Sleep(10 * 1000); } // 打印线程池的上下限阈值 private static void GetLimitation() { int maxWork, minWork, maxIO, minIO; // 得到阈值上限 ThreadPool.GetMaxThreads(out maxWork, out maxIO); // 得到阈值下限 ThreadPool.GetMinThreads(out minWork, out minIO); // 打印阈值上限 Console.WriteLine("线程池最多有{0}个工作者线程,{1}个IO线程", maxWork.ToString(), maxIO.ToString()); // 打印阈值下限 Console.WriteLine("线程池最少有{0}个工作者线程,{1}个IO线程", minWork.ToString(), minIO.ToString()); Console.WriteLine("------------------------------------"); } // 打印可用线程数量 private static void GetAvailable() { int remainWork, remainIO; // 得到当前可用线程数量 ThreadPool.GetAvailableThreads(out remainWork, out remainIO); // 打印可用线程数量 Console.WriteLine("线程池中当前有{0}个工作者线程可用,{1}个IO线程可用", remainWork.ToString(), remainIO.ToString()); Console.WriteLine("------------------------------------"); } }
该实例的执行结果如下图所示:
PS:上面代码示例在不同的计算机上运行可能会得到不同的结果,线程池中的可用数码不会再初始时达到最大值,事实上CLR会尝试以一定的时间间隔来逐一地创建新线程,但这个时间间隔非常短。