Thread Pooling
2010-12-15 13:45 RyanXiang 阅读(1247) 评论(3) 编辑 收藏 举报Thread Pooling(线程池)
无论你在什么时候创建一个线程,CPU都会花费几百毫秒来做一些准备,例如:分配私有变量的栈空间。一个线程在默认的情况下大概会花费1MB的内存。 线程池通过共享和循环使用线程,在一定程度上让这些性能损耗降到最小。这对于采取“分治”策略在多核CPU上并发执计算密集型程序是非常有效的一种方式。
线程池保证并行运行的线程维持在一个特定的范围,因为过多的活动线程将会耗尽系统的CPU资源和内存资源,一旦系统中活动的线程数超过线程池允许的最大数,线程池将把后来的线程放到工作队列中,只有当线程池中某个线程执行结束的时候,该线程才会从工作队列中启动。这种方式让并发应用成为可能,例如web服务器。可以通过以下几种方法来使用线程池:
Via the Task Parallel Library (from Framework 4.0)
By calling ThreadPool.QueueUserWorkItem
Via asynchronous delegates
Via BackgroundWorker
Task Parallel Library (from Framework 4.0)
ThreadPool.QueueUserWorkItem 方法
asynchronous delegates
BackgroundWorker
下面这些技术间接的使用了线程池 :
WCF, Remoting, ASP.NET, and ASMX Web Services application servers
System.Timers.Timer and System.Threading.Timer
Framework methods that end in Async, such as those on WebClient (the event-based asynchronous pattern), and most BeginXXX methods (the asynchronous programming model pattern)
PLINQ、
当使用线程池的时候需要注意如下几个地方:
1、你无法命名一个线程,这让调试变得非常困难(即使你在VS中跟踪调式也无济于事)。
2、线程池中的线程通常是后台线程。
3、在所有线程池线程都分配到任务后,线程池不会立即开始创建新的空闲线程。 为避免向线程分配不必要的堆栈空间,线程池按照一定的时间间隔创建新的空闲线程。 该时间间隔目前为半秒,调用ThreadPool.SetMinThreads可以解决这一问题。(参考:Optimizing the Thread Pool 线程池优化一节 )。4、你可以自由的修改线程池中线程的优先级---当它被放回线程池中优先级将被恢复重置。
你可以通过Thread.CurrentThread.IsThreadPoolThread属性来查询线程池中正在执行的线程。
1、
通过TPL来使用线程池(Entering the Thread Pool via TPL)
你可以通过Task Parallel Library中的Task类,来轻松的访问线程池。在Framework 4.0中对Task类做如下的介绍:如果你对4.0之前线程池相关的API非常熟悉的话,可以把非泛型类Task
当做ThreadPool.QueueUserWorkItem替代方案
,而把泛型类Task<TResult>理解成asynchronous delegates。TPL较之以前的API,不仅速度上有提升,而且更加方便灵活。
如果使用非泛型类Task,调用
Task.Factory.StartNew,并传入目标函数的委托:
1: static void Main() // The Task class is in System.Threading.Tasks
2: {
3: Task.Factory.StartNew (Go);
4: }
5: static void Go()
6: {
7: Console.WriteLine ("Hello from the thread pool!");
8: }
9:
Task.Factory.StartNew
返回一个Task类型的对象,你可以通过监听这个对象来做一些操作---例如:你可以调用它的Wait方法来等待它执行结束。当你调用Wait()方法以后,线程中未处理的异常将会向上抛到宿主线程上,如果你不调用的话,线程会和其他普通线程一样,
未处理的异常将会导致进程终止。
泛型类Task<TResult>是泛型类Task的一个子类。这个类可以从线程中返回一个值。
下面这个例子,我们将会通过
Task<TResult>把百度页面下载下来:
1: static void Main()
2: {
3: // Start the task executing:
4: Task<string> task = Task.Factory.StartNew<string>
5: ( () => DownloadString (http://www.baidu.com) );
6:
7: // We can do other work here and it will execute in parallel:
8: RunSomeOtherMethod();
9:
10: // When we need the task's return value, we query its Result property:
11: // If it's still executing, the current thread will now block (wait)
12: // until the task finishes:
13: string result = task.Result;
14: }
15:
16: static string DownloadString (string uri)
17: {
18: using (var wc = new System.Net.WebClient())
19: return wc.DownloadString (uri);
20: }
21:
当你调用Result属性的时候所有未处理的异常,都会包装成
AggregateException,抛向宿主线程。但是如果没有获取到返回值(同时没有调用Wait方法),未处理的异常仍会终止当前进程。
Task Parallel Library 还有许多新特性,能更加充分的利用多核处理器,我们将在第五部分详细进行讨论。
2、不通过TPL来使用线程池(Entering the Thread Pool Without TPL)
如果你使用的是.NET Framework4.0之前的版本,你将无法使用TPL来访问线程池. 所以必须使用ThreadPool.QueueUserWorkItem
和 asynchronous delegates来替代TPL。它们的区别在于asynchronous delegates 可以获取返回值,并且可以让调用者捕获到异常。
QueueUserWorkItem
下面代码演示了使用QueueUserWorkItem把一个简单的委托方法放到线程池中去运行:
1: static void Main()
2: {
3: ThreadPool.QueueUserWorkItem (Go);
4: ThreadPool.QueueUserWorkItem (Go, 123);
5: Console.ReadLine();
6: }
7:
8: static void Go (object data) // data will be null with the first call.
9: {
10: Console.WriteLine ("Hello from the thread pool! " + data);
11: }
12:
目标方法Go()必须接受一个object类型的参数(to satisfy the WaitCallback
delegate)。和ParameterizedThreadStart类似,通过这种方式可以方便的向方法传递参数。和Task类不同的是,
QueueUserWorkItem
不能通过返回来的object来帮助你管理异常。所以你必须在目标代码中处理异常。---未处理的异常仍会让程序终止。
Asynchronous delegates
ThreadPool.QueueUserWorkItem
方法不提供从执行结束线程获取返回值的功能。异步委托解决了这个问题,提供双向任何个数参数的传递。此外,未处理的异常可以轻松的抛给宿主线程(或者叫做EndInvoke)。所以它不需要显示的捕获异常。
不要把异步委托和异步方法(一些以Begin or End开头的方法)混淆了。虽然它们表面上看起来差不多,但是异步方法是用来解决更难的问题的,在《C# 4.0 in a Nutshell》书中的第23章有相关的介绍。
下面介绍了如何通过异步委托来执行一个任务:
- 使用你想同步运行的一个方法实例化一个委托。 (通常是一个已经预定义好的委托).
- 调用委托的BeginInvoke方法,保存返回值IAsyncResult,这个返回值在调用BeginInvoke时立即返回给调用者。在池中线程运行期间,你可以处理其他活动。
- 当你需要结果是,调用EndInvoke方法,把保存的IAsyncResult传递给该方法。
下面这个例子,我们通过异步在Main方法中调用一个返回字符串长度的函数:
1: static void Main()
2: {
3: Func<string, int> method = Work;
4: IAsyncResult cookie = method.BeginInvoke ("test", null, null);
5: //
6: // ... here's where we can do other work in parallel...
7: //
8: int result = method.EndInvoke (cookie);
9: Console.WriteLine ("String length is: " + result);
10: }
11:
12: static int Work (string s) { return s.Length; }
13:
EndInvoke
方法做了三件事情。1、如果异步委托没有执行完毕,则等待异步委托执行完毕。2、接收返回的参数。3、将未处理的异常抛向调用它的线程。
即使你使用异步委托调用的方法没有返回值,你也不得不调用EndInvoke方法。In practice, this is open to debate;
there are no
EndInvoke
police to administer punishment to noncompliers!如果你选择了不调用EndInvoke方法,你就必须考虑在目标方法中进行异常处理,从而避免未知的错误
你也可以在调用BeginInvoke的时候指定一个回调函数---一个以
IAsyncResult
对象作为参数并且执行完毕会自动调用的方法。
This allows the instigating thread to “forget” about the asynchronous delegate, but it requires a bit of extra work at the callback end:
1: static void Main()
2: {
3: Func<string, int> method = Work;
4: method.BeginInvoke ("test", Done, method);
5: // ...
6: //
7: }
8:
9: static int Work (string s) { return s.Length; }
10:
11: static void Done (IAsyncResult cookie)
12: {
13: var target = (Func<string, int>) cookie.AsyncState;
14: int result = target.EndInvoke (cookie);
15: Console.WriteLine ("String length is: " + result);
16: }
17:
The final argument to
BeginInvoke
is a user state object that populates the AsyncState
property of IAsyncResult
. It can contain anything you like; in this case, we’re using it to pass the method
delegate to the completion callback, so we can call EndInvoke
on it. BeginInvoke
最后一个参数,可以放入任何东西,我们传递一个函数,所以我们可以在函数中调用EndInvoke方法。
3、线程池优化(Optimizing the Thread Pool )
当任务被分配后,线程池启动一个线程。线程池管理器创建新的线程处理额外的负载,直到到达线程池最大的线程数。在一段充足的不活动期之后,线程池管理器处理掉那些不活动的线程,这使线程池有比较好的吞吐量。
你可以通过调用ThreadPool.SetMaxThreads
提高线程池的最大线程数;默认情况下:
- 1023 in Framework 4.0 in a 32-bit environment
- 32768 in Framework 4.0 in a 64-bit environment
- 250 per core in Framework 3.5
- 25 per core in Framework 2.0
(这些数字很大部分跟操作系统和硬件有关.)
你也可以通过调用ThreadPool.SetMinThreads
来设置线程池的最小线程数。最小线程数乍看起来你可能觉得没有什么用:CLR线程池的最小线程数量确保了在任务数量较少的情况下,新来的任务可以立即执行,从而省去了创建新线程的时间。
通常,默认的最小线程数和CPU的核数相同--这样可以最大限度的利用CPU资源。在web服务器环境中(例如IIS下的ASP.NET)最小线程数可能更高些--一般为50甚至更多。
最低线程数是如何工作的?
让线程池中的最小线程数增加到N个,并不意味着这N个线程就会被立刻创建--线程只会在需要的时候创建。相反,它会通知线程管理器在需要线程的时候,立刻创建N个线程。这也说明了为什么线程池在创建线程的时候会出现延时。
这是为了避免在很短的时间内由于大量的线程需要被创建,会突然让应用程序内存消耗急剧增大的问题。为了说明这个问题,假设有一个跑在四核CPU上的应用,在某时有40个任务同时运行,每个任务运行的时间为10ms.假设我们采取“分治策略”整个应用应该需要100ms。理想状态下,我们想让这40个任务运行在四个线程上(现在我们以如下方式进行测试):
1、我们不创建多余的线程,只用四个线程来完成任务。
2、我们创建多余的线程来消耗一些CPU时间和内存。
结果会发现让线程数和CPU核数相吻合,会让程序仅使用一小部分内存并且几乎没有性能损耗。所有的线程都被有效的利用了。
现在假设所有的线程的运行时间都超过10ms,并且每个任务都需要访问网络,在线程等待网络响应时,CPU是空闲的。这时线程管理器的线程节约策略就不太好了,应该继续创建新的线程,从而让所有的任务都并发执行。
幸运的是,线程池还有一个备份策略。如果队列中的任务等待时间超过半秒,则会创建一个新的线程。---一直创建到设置的最大线程数。
这半秒的延时是一把双刃剑。一方面,他不会一下子让内存消耗到40M甚至更多。另一方面,在线程阻塞时,创建新的线程会出现延时。例如查询数据库或者调用
WebClient.DownloadFile。
由于这个原因,你可以调用SetMinThreads方法来让其不在延时。例如:
ThreadPool.SetMinThreads (50, 50);
作者:塞北隐士
出处:http://www.cnblogs.com/xiangyun/
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。