C#|C# ThreadPool

在多线程的程序中,经常会出现两种情况:
1. 应用程序中线程把大部分的时间花费在等待状态,等待某个事件发生,然后给予响应。这一般使用ThreadPool(线程池)来解决。
2. 线程平时都处于休眠状态,只是周期性地被唤醒。这一般使用Timer(定时器)来解决。
线程 Thread 有两种:【前台线程】和【后台线程】,我们可以通过线程属性IsBackground 来指定线程的前后台属性(默认是前台线程);
两者的主要区别是:进程会等待所有的前台线程完成后再结束工作,但是如果只剩下后台线程,则会直接结束工作。
(一个重要注意事项是如果程序定义了一个不会完成的前台线程,主程序并不会正常结束)
.Net环境使用 Thread 建立的线程默认情况下为前台线程,即线程属性 IsBackground = false;在进程中,只要有一个前台线程未退出,进程就不会终止。主线程就是一个前台线程。后台线程不管线程是否结束,只要所有前台线程都退出(包括正常退出和异常退出)后,进程就会自动终止。一般后台线程用于处理时间较短的任务,如一个Web服务器中可以利用后台线程来处理客户端发过来的请求信息。而前台线程用来处理需要长时间等待的任务,如在Web服务器中的监听客户端请求的程序,或是定时对某些系统资源进行扫描的程序。
在什么情况下使用线程池?
1.单个任务处理的时间比较短
2.将需处理的任务的数量大
3.多线程的环境中,尽量采用线程池
线程池的大小
不管什么池,总有尺寸,ThreadPool也不例外。ThreadPool提供了4个方法来调整线程池的大小:
SetMaxThreads
GetMaxThreads
SetMinThreads
GetMinThreads
SetMaxThreads指定线程池最多可以有多少个线程,而GetMaxThreads自然就是获取这个值。SetMinThreads指定线程池中最少存活的线程的数量,而GetMinThreads就是获取这个值。
为何要设置一个最大数量和有一个最小数量呢?原来线程池的大小取决于若干因素,如虚拟地址空间的大小等。比如你的计算机是4G内存,而一个线程的初始堆栈大小为1M,那么你最多能创建4G/1M的线程(忽略操作系统本身以及其他进程内存分配);正因为线程有内存开销,所以如果线程池的线程过多而又没有被完全使用,那么这就是对内存的一种浪费,所以限制线程池的最大数是很make sense的。
那么最小数又是为啥?线程池就是线程的对象池,对象池的最大的用处是重用对象。为啥要重用线程,因为线程的创建与销毁都要占用大量的CPU时间。所以在高并发状态下,线程池由于无需创建销毁线程节约了大量时间,提高了系统的响应能力和吞吐量。最小数可以让你调整最小的存活线程数量来应对不同的高并发场景。
使用线程池的好处?
在多线程编程时,如果创建了过多的线程将会增加操作系统资源的占用,并且还要处理资源要求和潜在的占用冲突,并且使用了多线程之后将使代码的执行流程和资源竞争情况变得复杂,稍不留心就会产生Bug。在使用多线程编程时对需要同步的资源访问尤其需要注意,如系统资源(系统端口等)、共享资源(文件、窗口句柄等)、属于单个应用程序的资源(如全局、静态和实例字段或属性)。
【C#|C# ThreadPool】针对上面的情况,我们可以使用线程池来解决上面的大部分问题,跟使用单个线程相比,使用线程池有如下优点:
1、缩短应用程序的响应时间。因为在线程池中有的线程处于等待分配任务状态(只要没有超过线程池的最大上限),无需创建线程。
2、不必管理和维护生存周期短暂的线程,不用在创建时为其分配资源,在其执行完任务之后释放资源。
3、线程池会根据当前系统特点对池内的线程进行优化处理。
4、减少在创建和销毁线程上所花的时间以及系统资源的开销,如不使用线程池,有可能造成系统创建大量线程而导致消耗完系统内存以及”过度切换”。


总之使用线程池的作用就是减少创建和销毁线程的系统开销。在.NET中有一个线程的类ThreadPool,它提供了线程池的管理。
ThreadPool 类是一个静态类,你不能也不必要生成它的对象。而且一旦使用该方法在线程池中添加了一个项目,那么该项目将是无法取消的。这里你无需自己建立线程,只需把你要做的工作写成函数,然后作为参数传递给ThreadPool.QueueUserWorkItem()方法就行了,传递的方法就是依靠 WaitCallback 代理对象,而线程的建立、管理、运行等工作都是由系统自动完成的,你无须考虑那些复杂的细节问题。
ThreadPool是一个静态类,它没有构造函数,对外提供的函数也全部是静态的。其中有一个QueueUserWorkItem方法,它有两种重载形式,如下:
public static bool QueueUserWorkItem(WaitCallback callBack):将方法排入队列以便执行。此方法在有线程池线程变得可用时执行。
public static bool QueueUserWorkItem(WaitCallback callBack,Object state):将方法排入队列以便执行,并指定包含该方法所用数据的对象。此方法在有线程池线程变得可用时执行。
QueueUserWorkItem方法中使用的的WaitCallback参数表示一个delegate,它的声明如下:
public delegate void WaitCallback(Object state)
如果需要传递任务信息可以利用WaitCallback中的state参数,类似于ParameterizedThreadStart委托。
ThreadPool 的用法:
首先程序创建了一个 ManualResetEvent 对象,该对象就像一个信号灯,可以利用它的信号来通知其它线程。
ManualResetEvent 对象有几个重要的方法:
初始化该对象时,用户可以指定其默认的状态(有信号/无信号);
在初始化以后,该对象将保持原来的状态不变,直到它的 Reset() 或者 Set() 方法被调用:
Reset():
将其设置为无信号状态;
Set():
将其设置为有信号状态。
WaitOne():
使当前线程挂起,直到 ManualResetEvent 对象处于有信号状态,此时该线程将被激活。然后,程序将向线程池中添加工作项,这些以函数形式提供的工作项被系统用来初始化自动建立的线程。当所有的线程都运行完了以后,ManualResetEvent.Set() 方法被调用,因为调用了 ManualResetEvent.WaitOne() 方法而处在等待状态的主线程将接收到这个信号,于是它接着往下执行,完成后边的工作。
我们在一个程序中创建一个线程,安排给它一个任务,便交由操作系统来调度执行。操作系统会管理系统中所有的线程,并且使用一定的方式进行调度。什么是“调度”?调度便是控制线程的状态:执行,等待等等。我们都知道,从理论上来说有多少个处理单元(如2 * 2 CPU的机器便有4个处理单元),就表示操作系统可以同时做几件事情。但是线程的数量会远远超过处理单元的数量,因此操作系统为了保证每个线程都被执行,就必须等一个线程在某个处理器上执行到某个情况的时候,“换”一个新的线程来执行,这便是所谓的“上下文切换(Context Switch)”。至于造成上下文切换的原因也有多种,可能是某个线程的逻辑决定的,如遇上锁,或主动进入休眠状态(调用Thread.Sleep方法),但更有可能是操作系统发现这个线程“超时”了。在操作系统中会定义一个“时间片(timeslice)“,当发现一个线程执行时间超过这个时间,便会把它撤下,换上另外一个。这样看起来,多个线程——也就是多个任务在同时运行了。值得一提的是,对于Windows操作系统来说,它的调度单元是线程,这和线程究竟属于哪个进程并没有关系。
QueueUserWorkItem这个技术存在许多限制。其中最大的问题是没有一个内建的机制让你知道操作在什么时候完成,也没有一个机制在操作完成时获得一个返回值,这些问题使得我们都不敢启用这个技术。

    推荐阅读