代码改变世界

C#多线程(二)

2014-03-27 17:20  OshynSong  阅读(995)  评论(0编辑  收藏  举报

一、线程池

每次创建一个线程,都会花费几百微秒级别的时间来创建一个私有的局部栈,每个线程默认使用1M的内存。这个可以在使用Thread类的构造函数时设置:

 

[csharp] view plaincopyprint?在CODE上查看代码片派生到我的代码片
 
  1. new Thread(new ThreadStart(Go), 2);  
  2. new Thread(new ParameterizedThreadStart(Go("hello")), 3);  

提供的两种构造函数方式都提供了对应的设置线程局部栈的大小。线程池通过共享和回收线程的方式来分配这些内存,这样可以使多线程运行在一个非常细粒度级别上而不影响性能。这对于充分利用多核处理器,使用分而治之的方式进行密集型计算的程序中很有用。同时线程池维护一个所有同时运行的工作线程总数的上限,如果有过多的活动线程就会加重操作系统的负担,使诸如CPU缓存失效等问题,当达到这个上限后,就要进行排队。这个线程队列使得任意并发的应用成为可能,如Web服务器就是这种原理。

 

有多种方式进入线程池:

 

  • 通过Task Parallel Library(.NET 4  TPL)
  • 通过调用ThreadPool.QueueUserWorkItem
  • 通过异步委托方式
  • 使用BackgroundWorker
下面的应用间接地使用了线程池:
  • WCF、Remoting、ASP.NET、ASMX webservice
  • System.Timers.Timer和System.Threading.Timer
  • .NET中以Async结束命名的方法
  • PLINQ
注意事项:
  • 不能对放入线程池中的线程命名,这会使得调试更加困难
  • 线程池中的线程总是后台线程
  • 阻塞线程池中的线程困难引发额外的延迟
可以使用下面的方式查询当前线程是否在线程池中:
[csharp] view plaincopyprint?在CODE上查看代码片派生到我的代码片
 
  1. CurrentTread.IsThreadPoolThread  

二、进入线程池

1、使用TPL进入
使用TPL中的Task类就可以很简单的进入,使用Task.Factory.StartNew方法,传递一个目标函数的委托即可:
[csharp] view plaincopyprint?在CODE上查看代码片派生到我的代码片
 
  1. static void Main(string[] args)  
  2. {  
  3.     Task.Factory.StartNew(Go);  
  4.   
  5.     Console.WriteLine("Is Thread Pool: " + Thread.CurrentThread.IsThreadPoolThread.ToString());  
  6.     Console.ReadKey();  
  7. }  
  8. static void Go()  
  9. {  
  10.     Console.Write("Is Thread Pool: " + Thread.CurrentThread.IsThreadPoolThread.ToString());  
  11. }  

Task.Factory.StartNew返回一个Task对象可以用来监控这个任务。
同时可以使用泛型类Task<TResult>,如下:
[csharp] view plaincopyprint?在CODE上查看代码片派生到我的代码片
 
  1. static void Main(string[] args)  
  2. {  
  3.     Task<string> task = Task.Factory.StartNew<string>(Go);  
  4.       
  5.     Console.WriteLine("Is Thread Pool: " + Thread.CurrentThread.IsThreadPoolThread.ToString());  
  6.   
  7.     if (task.IsCompleted)  
  8.     {  
  9.         string result = task.Result;  
  10.         Console.WriteLine(task.IsCompleted.ToString() +  result);  
  11.     }  
  12.     Console.ReadKey();  
  13. }  
  14. static string Go()  
  15. {  
  16.     return Thread.CurrentThread.IsThreadPoolThread.ToString();  
  17. }  

如果在工作线程中出现异常,当获取Task的Result属性时会重新引发AggregateException异常,如果没有查询Result或者没有调用Wait方法,所有未处理的异常都会终止进程执行。
2、不使用TPL
对于.NET 4.0以前的版本是无法使用TPL的,必须使用ThreadPool.QueueUserWorkItem(类似Task类功能)和异步委托(类似Task<TResult>),但是这两个没有前面所述方法快、也没有那么方便和很好的扩展性。两种方式的使用如下:
[csharp] view plaincopyprint?在CODE上查看代码片派生到我的代码片
 
  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. }  
[csharp] view plaincopyprint?在CODE上查看代码片派生到我的代码片
 
  1. static void Main()  
  2. {  
  3.   Func<stringint> 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<stringint>) cookie.AsyncState;  
  14.   int result = target.EndInvoke (cookie);  
  15.   Console.WriteLine ("String length is: " + result);  
  16. }  

三、线程池的优化

线程池拥有的最大线程数可以通过ThreadPool.SetMaxThreads设置,默认值如下:
  • 32位的.NET4.0环境为1023个
  • 64位的.NET4.0环境为32768个
  • .NET 3.5为每个CPU核 250个
  • .NET2.0 为每个CPU核25个
线程池管理器在分配任务时通过添加新的线程来应对额外的工作量,当达到极限后开始排队。在非活动的闲置期,管理器可以删除一些可疑的线程从而可以得到更好的吞吐量。当然也可以使用Thread.SetMinThreads来设置最小的线程数目:最小线程数是优化线程池的高级方式,这个将指导管理器对于线程的分配没有延迟。提高最小数目可以当在有阻塞线程时提高并发量。
提高线程的最小数目并不能保证至少有相应个线程在线程池中,线程只会在有需要的时候创建。同时这个会指导池管理器创建到最小数目的线程。线程池在分配线程时会有半秒的延迟,之所以要有这个延迟就是为了防止突然的大量线程分配导致应用程序内存占用过大。线程池对于队列保持等待超过半秒时就会通过创建新的线程来响应请求,没半秒创建一个只到达到最大线程数。
这个半秒的延迟是有利也有弊的,弊端在于当一个阻塞的线程出现时并不需要延迟半秒,例如一个查询数据库或者下载网页的线程。这时需要告诉池管理器不要延迟进程线程分配,因此就需要用到SetMinThreads来设置:
[csharp] view plaincopyprint?在CODE上查看代码片派生到我的代码片
 
  1. ThreadPool.SetMinThreads(50,50);