《CLR via C#》读书笔记 之 计算限制的异步操作
2014-07-06
26.1 CLR线程池基础
如25章所述,创建和销毁线程是一个比较昂贵的操作:
- 太多的线程也会浪费内存资源。
- 由于操作系统必须调度可运行的线程并执行上下文切换,所以太多的线程还有损于性能。
为了改善这个情况,CLR使用了代码来管理它自己的线程池。可将线程池想像成可由你的应用程序使用的一个线程集合。每个进程都有一个线程池,它在各个应用程序域(AppDomain)是共享的.
线程池是如何工作的:
- CLR初始化时,线程池是没有线程的。在内部,线程池维护了一个操作请求队列。应用程序想执行一个异步操作时,就调用某个方法,将一个记录项(entry)追加到线程池的队列中。线程池的代码从这个队列中提取记录项,将这个记录项派遣(dispatch)给一个线程池线程。如果线程池中没有线程,就创建新的线程。创建线程要产生一定的性能损失。然而,当线程池完成任务后,线程不会被销毁。相反,线程会返回线程池,在那里进入空闲状态,等待响应另一个请求。由于线程不销毁自身,所以不再产生额外的性能损失。
- 如果你的应用程序向线程池发出许多请求,线程池会尝试只用一个线程来服务所有的请求。然而,如果你的应用程序发出请求的速度超过了线程池处理它们的速度,就会创建额外的线程。最终,你的应用程序所有请求都可能有少量的线程处理,所有线程池不必创建大量的线程。
- 如果你的应用程序停止向线程池发出请求,池中含有大量空闲的线程。这是对内存资源的一种浪费。所以,当一个线程池线程空闲一段时间以后,线程会自己醒来终止自己以释放资源。
在内部,线程池将自己的线程划分为工作者(Worker)线程和I/O线程。应用程序要求线程池执行一个异步的计算限制操作时(这个操作可能发起一个I/O限制的操作),使用的就是工作者线程。I/O线程用于通知你的代码一个异步I/O限制操作已经完成,具体的说,这意味着使用"异步编程模型"发出I/O请求,比如访问文件、网络服务器、数据库等等。
26.2 执行简单的计算限制操作
将一个异步的、计算限制的操作放到一个线程池的队列中,通常可以调用ThreadPool类定义的以下方法之一:
static Boolean QueueUserWorkItem(WaitCallback callBack);
static Boolean QueueUserWorkItem(WaitCallback callBack,Object state);
这些方法向线程池的队列中添加一个"工作项"(work item)以及可选的状态数据, 如果此方法成功排队,则为 true;如果无法将该工作项排队,则引发 OutOfMemoryException。工作项其实就是由callBack参数标识的一个方法,该方法将由线程池线程调用。可通过state实参(状态数据)向方法传递一个参数。无state参数的那个版本的QueueUserWorkItem则向回调方法传递null。最终,池中的某个线程会处理工作项,造成你指定的方法被调用。你写的回调方法必须匹配System.Threading.WaitCallBack委托类型,它的定义如下:
delegate void WaitCallback(Object state);
注意:WaitCallback委托、TimerCallback委托(在本章26.8节“执行时计算限制操作”中讨论)和ParameterizedThreadStart委托(在第25章“线程基础”中讨论)是完全一致的。因此,只要定义一个和该签名匹配的方法后,使用ThreadPool.QueueUserWorkItem方法,使用System.Threading.Timer对象,或着使用System.Threading.Thread对象,都一个调用这个方法。
以下演示了如何让一个线程池线程以异步方式调用一个方法:
using System;
using System.Threading;
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Main thread: queuing an asynchronous operation");
ThreadPool.QueueUserWorkItem(ComputeBoundOp, );
Console.WriteLine("Main thread: Doing other work here...");
Thread.Sleep(); // 模拟其它工作 (10 秒钟)
//Console.ReadLine();
} // 这是一个回调方法,必须和WaitCallBack委托签名一致
private static void ComputeBoundOp(Object state)
{
// 这个方法通过线程池中线程执行
Console.WriteLine("In ComputeBoundOp: state={0}", state);
Thread.Sleep(); // 模拟其它工作 (1 秒钟) // 这个方法返回后,线程回到线程池,等待其他任务
}
}
编译运行的结果是:
Main thread: queuing an asynchronous operation
Main thread: Doing other work here...
In ComputeBoundOp: state=5
但有时也会得到一下输出:
Main thread: queuing an asynchronous operation
In ComputeBoundOp: state=5
Main thread: Doing other work here...
之所以有两种输出结果,是因为这两个方法相互之间是异步运行的。由Windows调度器决定先调度哪一个线程。
26.3 执行上下文
每个线程都关联了一个执行上下文数据结构。执行上下文(execution context)包括的东西有:
- 安全设置:压缩栈、Thread的Principal属性[指示线程的调度优先级]和Windows身份;
- 宿主设置:参见System.Threading.HostExecutionContextManager[提供使公共语言运行时宿主可以参与执行上下文的流动(或移植)的功能];
- 逻辑调用上下文数据:参见System.Runtime.Remoting.Messaging.CallContext[提供与执行代码路径一起传送的属性集]的LogicalSetData[将一个给定对象存储在逻辑调用上下文中并将该对象与指定名称相关联]和LogicalGetData[从逻辑调用上下文中检索具有指定名称的对象]。
线程执行代码时,有的操作会受到线程的执行上下文设置(尤其是安全设置)的影响。理想情况下,每当一个线程(初始线程)使用另一个线程(辅助线程)执行任务时,前者的执行上下文应该"流动"(复制)到辅助线程。这就确保辅助线程执行的任何操作使用的都是相同的安全设置和宿主设置。还确保了初始线程的逻辑调用上下文可以在辅助线程中使用。
默认情况下,CLR自动造成初始线程的执行上下文会"流动"(复制)到任何辅助线程。这就是将上下文信息传输到辅助线程,但这对损失性能,因为执行上下文中包含大量信息,而收集这些信息,再将这些信息复制到辅助线程,要耗费不少时间。如果辅助线程又采用更多的辅助线程,还必须创建和初始化更多的执行上下文数据结构。
System.Threading命名空间中有一个ExecutionContext类[管理当前线程的执行上下文],它允许你控制线程的执行上下文如何从一个线程"流动"(复制)到另一个线程。下面展示了这个类的样子:
public sealed class ExecutionContext : IDisposable, ISerializable
{
[SecurityCritical]
//取消执行上下文在异步线程之间的流动
public static AsyncFlowControl SuppressFlow();
//恢复执行上下文在异步线程之间的流动
public static void RestoreFlow();
//指示当前是否取消了执行上下文的流动。
public static bool IsFlowSuppressed(); //不常用方法没有列出
}
可用这个类阻止一个执行上下文的流动,从而提升应用程序的性能。对于服务器应用程序,性能的提升可能非常显著。但是,客户端应用程序的性能提升不了多少。另外,由于SuppressFlow方法用[SecurityCritical]attribute进行了标识,所以在某些客户端应用程序(比如Silverlight)中是无法调用的。当然,只有在辅助线程不需要或者不防问上下文信息时,才应该组织执行上下文的流动。如果初始线程的执行上下文不流向辅助线程,辅助线程会使用和它关联起来的任何执行上下文。在这种情况下,辅助线程不应该执行要依赖于执行上下文状态(比如用户的Windows身份)的代码。
注意:添加到逻辑调用上下文的项必须是可序列化的。对于包含了逻辑调用上下文数据线的一个执行上下文,如果让它流动,可能严重损害性能,因为为了捕捉执行上下文,需对所有数据项进行序列化和反序列化。
下例展示了向CLR的线程池队列添加一个工作项的时候,如何通过阻止执行上下文的流动来影响线程逻辑调用上下文中的数据:
static void Main(string[] args)
{
// 将一些数据放到Main线程的逻辑调用上下文中
CallContext.LogicalSetData("Name", "Jeffrey"); // 线程池能访问到逻辑调用上下文数据,加入到程序池队列中
ThreadPool.QueueUserWorkItem(
state => Console.WriteLine("Name={0}", CallContext.LogicalGetData("Name"))); // 现在阻止Main线程的执行上下文流动
ExecutionContext.SuppressFlow(); //再次访问逻辑调用上下文的数据
ThreadPool.QueueUserWorkItem(
state => Console.WriteLine("Name={0}", CallContext.LogicalGetData("Name"))); //恢复Main线程的执行上下文流动
ExecutionContext.RestoreFlow();
}
会得到一下结果:
Name=Jeffrey
Name=
虽然现在我们讨论的是调用ThreadPool.QueueUserWorkItem时阻止执行上下文的流动,但在使用Task对象(参见26.5节”任务“),以及在发起异步I/O操作(参见第27章“I/o限制的异步操作”)时,这个技术也会用到。