线程池的作用
在上一篇中我们了解了创建和销毁线程是一个昂贵的操作,要耗费大量的时间,太多的线程会浪费内存资源,当线程数量操作计算机CPU的数量后操作系统必须调度可运行的线程并执行上下文切换,所有太多的线程还会影响性能,那么有没有办法让线程可以重复使用了,让线程干完活之后不用销毁,把它放在一个容器中, 等待下次有任务的时候在从容器中取出来就行了,这样就避免了创建和销毁所带来的性能损耗,所有线程池的作用总结起来就是:因为创建一个线程的代价较高,因此我们使用线程池设法复用线程。
线程基础
每个CLR 拥有一个线程池,这个线程池由CLR控制的APPDomain 共享。在线程池内部,它自己维护着一个操作请求队列,应用程序需要执行某个任务时,就需要调用线程池的一个方法(通常是QueueUserWorkItem 方法)将任务添加到线程池工作项中,线程池就会将任务分派给一个线程池线程处理,如果线程池中没有线程,就会创建一个线程来处理这个任务。当任务执行完成以后,这个线程会回到线程池中处于空闲状态,等待下一个执行任务。由于线程不会销毁,所以使用线程池线程在执行任务的速度上会更快。
如果线程池中的任务过多超过了现有线程的处理能力时,线程池就会根据需要在创建更多的线程。由于每个线程都要占用一定的内存资源,所以当线程池空闲线程(长时间不执行任务的线程)过多时,线程池中线程会自动醒来销毁多余的空闲线程,以减少资源的使用。
在线程池内部,所有线程都是后台线程并且调度优先级都为普通(ThreadPriority.Normal),这些线程分为工作者或I/O线程,当线程池线程执行的任务是一个复杂的计算任务时,使用的就是工作者线程。如果执行的任务与I/O相关,就会使用I/O线程。
如何使用线程池
ThreadPool 类是一个静态类型类,使用ThreadPool 类执行异步时通常调用ThreadPool 的 QueueUserWorkItem 方法,这个方法有一个重载版本,如下
public static bool QueueUserWorkItem(WaitCallback callBack) { StackCrawlMark stackCrawlMark = StackCrawlMark.LookForMyCaller; return ThreadPool.QueueUserWorkItemHelper(callBack, null, ref stackCrawlMark, true); } public static bool QueueUserWorkItem(WaitCallback callBack, object state) { StackCrawlMark stackCrawlMark = StackCrawlMark.LookForMyCaller; return ThreadPool.QueueUserWorkItemHelper(callBack, state, ref stackCrawlMark, true); }
QueueUserWorkItem 方法接受一个WaitCallback 类型的委托作为回调方法以及可以选择传递一个线程池线程执行回调方法时所需要的数据对象。
WaitCallback 委托类型的定义如下:
namespace System.Threading { [__DynamicallyInvokable, ComVisible(true)] public delegate void WaitCallback(object state); }
static void Main(string[] args) { Console.WriteLine("主线程开始执行任务,线程ID:{0}",Thread.CurrentThread.ManagedThreadId); //使用 ThreadPool.QueueUserWorkItem 方法将一个异步任务添加到线程池任务队列中, //可以为线程池线程执行方法时传递一个数据对象, //如果不需要传递数据可以使用QueueUserWorkItem只有WaitCallback一个参数类型的版本, //或传递null ThreadPool.QueueUserWorkItem(ThreadMethod,null); Console.WriteLine("主线程执行其他任务。线程ID:{0}", Thread.CurrentThread.ManagedThreadId); //使调用线程睡眠2000毫秒,等待线程池线程执行完成。 Thread.Sleep(2000); Console.WriteLine("主线程继续执行任务。线程ID:{0}", Thread.CurrentThread.ManagedThreadId); } public static void ThreadMethod(object state) { Console.WriteLine("我是由线程池创建的线程,线程ID:{0}",Thread.CurrentThread.ManagedThreadId); for (int i = 0; i < 10; i++) { Thread.Sleep(200); Console.WriteLine(i); } } 输出 主线程开始执行任务,线程ID:1 主线程执行其他任务。线程ID:1 我是由线程池创建的线程,线程ID:3 0 1 2 3 4 5 6 7 8 主线程继续执行任务。线程ID:1 请按任意键继续. . .
线程池对线程的管理
线程池中的线程是由工作者线程和I/O线程组成的, CLR允许开发人员设置线程池需要创建的最大线程数量,但实践证明,线程池永远都不应该为池中的线程数设置上限,因为可能发生饥饿或死锁,假定队列中有1000个工作项,但这些工作项全都因为一个事件而阻塞,等第1001个工作项发出信号才能解除阻塞,如果设置了最大1000个线程,第1001个工作项就不会执行,所有1000个线程都会一直阻塞,最终用户将被终止应用程序,并丢失他们的所有未保存的工作。
//设置可以同时处于活动状态的线程池的请求数目。 // 所有大于此数目的请求将保持排队状态,直到线程池线程变为可用。 public static bool SetMaxThreads(int workerThreads, int completionPortThreads); // 发出新的请求时,在切换到管理线程创建和销毁的算法之前设置线程池按需创建的线程的最小数量。 public static bool SetMinThreads(int workerThreads, int completionPortThreads); //检索可以同时处于活动状态的线程池请求的数目。 所有大于此数目的请求将保持排队状态,直到线程池线程变为可用。 public static void GetMaxThreads(out int workerThreads, out int completionPortThreads); //发出新的请求时,在切换到管理线程创建和销毁的算法之前检索线程池按需创建的线程的最小数量。 public static void GetMinThreads(out int workerThreads, out int completionPortThreads); //获得最大线程池线程数和当前运行线程数之间的差值。 public static void GetAvailableThreads(out int workerThreads, out int completionPortThreads);
虽然ThreadPool类提供了上诉方法,但建议不要调用上诉中的方法,限制线程池的线程数,一般都只造成应用程序的性能变的更差,而不会更好,使用方法
static void Main(string[] args) { int worker, io; //获得线程池默认最大线程数 ThreadPool.GetMaxThreads(out worker, out io); Console.WriteLine("1、CLR线程池默认最大线程数据,工作者线程数:{0},IO线程数:{1}", worker, io); //设置线程池最大线程数 ThreadPool.SetMaxThreads(100, 100); ThreadPool.QueueUserWorkItem(state => { Thread.Sleep(2000); Console.WriteLine("4、线程池线程开始执行异步任务。线程ID:{0}", Thread.CurrentThread.ManagedThreadId); }); Console.WriteLine("2、自定义设置线程池默认最大线程数据后,工作者线程数:{0},IO线程数:{1}", worker, io); //获得最大线程池线程数和当前运行线程数之间的差值。 ThreadPool.GetAvailableThreads(out worker, out io); Console.WriteLine("3、获得最大线程池线程数和当前运行线程数之间的差值,工作者线程:{0},IO线程:{1}", worker, io); Console.Read(); } 1、CLR线程池默认最大线程数据,工作者线程数:2047,IO线程数:1000 2、自定义设置线程池默认最大线程数据后,工作者线程数:2047,IO线程数:1000 3、获得最大线程池线程数和当前运行线程数之间的差值,工作者线程:99,IO线程:100 4、线程池线程开始执行异步任务。线程ID:3