-
C#线程 入门-线程池
线程池
每当启动线程时,都会花费数百微秒来组织诸如新鲜的私有局部变量堆栈之类的事情。每个线程(默认情况下)也消耗大约1 MB的内存。线程池通过共享和回收线程来减少这些开销,从而允许在非常细粒度的级别上应用多线程,而不会影响性能。当利用多核处理器以“分而治之”的方式并行执行计算密集型代码时,这很有用。
线程池还限制了将同时运行的工作线程总数。过多的活动线程限制了操作系统的管理负担,并使CPU缓存无效。一旦达到限制,作业将排队并仅在另一个作业完成时才开始。这使任意并发的应用程序(例如Web服务器)成为可能。 (异步方法模式是一种高级技术,通过高效利用池化线程来进一步实现这一点;我们在C#4.0的第23章“ Nutshell”中对此进行了描述)。
有多种进入线程池的方法:
- 通过任务并行库(来自Framework 4.0)
- 通过调用ThreadPool.QueueUserWorkItem
- 通过异步委托
- 通过BackgroundWorker
以下构造间接使用线程池:
- WCF,远程,ASP.NET和ASMX Web服务应用程序服务器
- System.Timers.Timer和System.Threading.Timer
- 以Async结尾的框架方法,例如WebClient上的框架方法(基于事件的异步模式),以及大多数BeginXXX方法(异步编程模型模式)
- PLINQ
任务并行库(TPL)和PLINQ具有足够的功能和高级功能,即使在线程池不重要的情况下,您也希望使用它们来协助多线程。我们将在第5部分中详细讨论这些内容。现在,我们将简要介绍如何使用Task类作为在池线程上运行委托的简单方法。
使用池线程时需要注意以下几点:
- 您无法设置池线程的名称,从而使调试更加困难(尽管您可以在Visual Studio的“线程”窗口中进行调试时附加说明)。
- 池线程始终是后台线程(这通常不是问题)。
- 除非您调用ThreadPool.SetMinThreads(请参阅优化线程池),否则阻塞池中的线程可能会在应用程序的早期阶段触发额外的延迟。
- 您可以自由更改池线程的优先级-在释放回池时,它将恢复为正常。
您可以通过Thread.CurrentThread.IsThreadPoolThread属性查询当前是否在池化线程上执行。
通过TPL进入线程池
您可以使用“任务并行库”中的“任务”类轻松地输入线程池。 Task类是在Framework 4.0中引入的:如果您熟悉较早的构造,请考虑将非通用Task类替换为ThreadPool.QueueUserWorkItem,而将通用Task <TResult>替换为异步委托。与旧版本相比,新版本的结构更快,更方便且更灵活。
要使用非泛型Task类,请调用Task.Factory.StartNew,并传入目标方法的委托:
1
2
3
4
5
6
7
8
9
|
static void Main() // The Task class is in System.Threading.Tasks { Task.Factory.StartNew (Go); } static void Go() { Console.WriteLine ( "Hello from the thread pool!" ); } |
Task.Factory.StartNew返回一个Task对象,您可以使用该对象来监视任务-例如,您可以通过调用其Wait方法来等待它完成。
调用任务的Wait方法时,所有未处理的异常都可以方便地重新抛出到主机线程中。 (如果您不调用Wait而是放弃任务,则未处理的异常将像普通线程一样关闭进程。)
通用Task <TResult>类是非通用Task的子类。它使您可以在完成执行后从任务中获取返回值。在下面的示例中,我们使用Task <TResult>下载网页:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
static void Main() { // Start the task executing: Task< string > task = Task.Factory.StartNew< string > ( () => DownloadString ( "http://www.linqpad.net" ) ); // We can do other work here and it will execute in parallel: RunSomeOtherMethod(); // When we need the task's return value, we query its Result property: // If it's still executing, the current thread will now block (wait) // until the task finishes: string result = task.Result; } static string DownloadString ( string uri) { using ( var wc = new System.Net.WebClient()) return wc.DownloadString (uri); } |
(突出显示<string>类型的参数是为了清楚:如果我们省略它,则可以推断出它。)
查询包含在AggregateException中的任务的Result属性时,所有未处理的异常都会自动重新抛出。但是,如果您无法查询其Result属性(并且不调用Wait),则任何未处理的异常都会使该过程失败。
任务并行库具有更多功能,特别适合利用多核处理器。我们将在第5部分中继续讨论TPL。
不通过TPL进入线程池
如果目标是.NET Framework的早期版本(4.0之前),则不能使用任务并行库。相反,您必须使用一种较旧的结构来输入线程池:ThreadPool.QueueUserWorkItem和异步委托。两者之间的区别在于异步委托使您可以从线程返回数据。异步委托也将任何异常封送回调用方。
QueueUserWorkItem
要使用QueueUserWorkItem,只需使用要在池线程上运行的委托调用此方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
static void Main() { ThreadPool.QueueUserWorkItem (Go); ThreadPool.QueueUserWorkItem (Go, 123); Console.ReadLine(); } static void Go ( object data) // data will be null with the first call. { Console.WriteLine ( "Hello from the thread pool! " + data); } Hello from the thread pool! Hello from the thread pool! 123 |
我们的目标方法Go必须接受单个对象参数(以满足WaitCallback委托)。就像使用ParameterizedThreadStart一样,这提供了一种将数据传递给方法的便捷方法。与Task不同,QueueUserWorkItem不会返回对象来帮助您随后管理执行。另外,您必须在目标代码中显式处理异常-未处理的异常将使程序瘫痪。