代码改变世界

Thread Pooling

2010-12-15 13:45  RyanXiang  阅读(1238)  评论(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章有相关的介绍。

下面介绍了如何通过异步委托来执行一个任务:

  1. 使用你想同步运行的一个方法实例化一个委托。 (通常是一个已经预定义好的委托).
  2. 调用委托的BeginInvoke方法,保存返回值IAsyncResult,这个返回值在调用BeginInvoke时立即返回给调用者。在池中线程运行期间,你可以处理其他活动。
  3. 当你需要结果是,调用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);