首页 > 学院 > 开发设计 > 正文

[CLR via C#]26. 计算限制的异步操作

2019-11-17 03:15:50
字体:
来源:转载
供稿:网友

[CLR via C#]26. 计算限制的异步操作

  一、CLR线程池基础

前面说过,创建和销毁线程是一个比较昂贵的操作,太多的线程也会浪费内存资源。由于操作系统必须调度可运行的线程并执行上下文切换,所以太多的线程还有损于性能。为了改善这个情况,CLR使用了代码来管理它自己的线程池。可将线程池想像成可由你的应用程序使用的一个线程集合。每个进程都有一个线程池,它在各个应用程序域(AppDomain)是共享的.

CLR初始化时,线程池是没有线程的。在内部,线程池维护了一个操作请求队列。应用程序想执行一个异步操作时,就调用某个方法,将一个记录项(entry)追加到线程池的队列中。线程池的代码从这个队列中提取记录项,将这个记录项派遣(dispatch)给一个线程池线程。如果线程池中没有线程,就创建新的线程。创建线程要产生一定的性能损失。然而,当线程池完成任务后,线程不会被销毁。相反,线程会返回线程池,在那里进入空闲状态,等待响应另一个请求。由于线程不销毁自身,所以不再产生额外的性能损失。 如果你的应用程序向线程池发出许多请求,线程池会尝试只用一个线程来服务所有的请求。然而,如果你的应用程序发出请求的速度超过了线程池处理它们的速度,就会创建额外的线程。最终,你的应用程序所有请求都可能有少量的线程处理,所有线程池不必创建大量的线程。 如果你的应用程序停止向线程池发出请求,池中含有大量空闲的线程。这是对内存资源的一种浪费。所以,当一个线程池线程空闲一段时间以后,线程会自己醒来终止自己以释放资源。 线程终止自己时,会产生一定的性能损失。然后,线程终止自己的情况下,表明你的应用程序本身就没有做什么事情,所以这个性能损失关系不大。 在内部,线程池将自己的线程划分为工作者(Worker)线程I/O线程。应用程序要求线程池执行一个异步的计算限制操作时(这个操作可能发起一个I/O限制的操作),使用的就是工作者线程。I/O线程用于通知你的代码一个异步I/O限制操作已经完成,具体的说,这意味着使用"异步编程模型"发出I/O请求,比如访问文件、网络服务器、数据库等等。    二、执行简单的计算限制操作 将一个异步的、计算限制的操作放到一个线程池的队列中,通常可以调用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);
以下演示了如何让一个线程池线程以异步方式调用一个方法:
class PRogram    {        static void Main(string[] args)        {            Console.WriteLine("Main thread: queuing an asynchronous Operation");            ThreadPool.QueueUserWorkItem(ComputeBoundOp, 5);            Console.WriteLine("Main thread: Doing other work here...");            Thread.Sleep(10000);  // 模拟其它工作 (10 秒钟)            //Console.ReadLine();        }         // 这是一个回调方法,必须和WaitCallBack委托签名一致        private static void ComputeBoundOp(Object state)        {            // 这个方法通过线程池中线程执行            Console.WriteLine("In ComputeBoundOp: state={0}", state);            Thread.Sleep(1000);  // 模拟其它工作 (1 秒钟)             // 这个方法返回后,线程回到线程池,等待其他任务        }    } 
我编译运行的结果是:Main thread: queuing an asynchronous operationMain thread: Doing other work here...In ComputeBoundOp: state=5 但有时也会得到一下输出:Main thread: queuing an asynchronous operationIn ComputeBoundOp: state=5Main thread: Doing other work here... 之所以有两种输出结果,是因为这两个方法相互之间是异步运行的。由Windows调度器决定先调度哪一个线程。  三、执行上下文 每个线程都关联了一个执行上下文数据结构。执行上下文(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的线程池队列添加一个工作项的时候,如何通过阻止执行上下文的流动来影响线程逻辑调用上下文中的数据:
class Program    {        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();             //再次访问逻辑调用上下文的数据            ThreadPool.QueueUserWorkItem(               state => Console.WriteLine("Name={0}", CallContext.LogicalGetData("Name")));            Console.Read();        }    }
 会得到一下结果:Name=JeffreyName=Name=Jeffrey 虽然现在我们讨论的是调用ThreadPool.QueueUserWorkItem时阻止执行上下文的流动,但在使用Task对象(以后会提到),以及在发起异步I/O操作(以后会提到)时也会用到。  四、协作式取消 Microsoft .NET Framework提供了一个标准的取消操作模式。这个模式是协作式的,意味着你想取消的操作必须显式的支持取消。换言之,无论执行操作的代码,还是试图取消操作的代码,都必须使用本节提到的类型。对于长时间 运行的计算限制操作来说,支持取消是一件非常"棒"的事。所以,你应该考虑为自己的计算限制操作添加取消能力。 首先,先解释一下FCL提供的两个主要类型,它们是标准协作式取消模式的一部分。 为了取消一个操作,首先必须创建一个System.Thread.CancellationTokenSource[通知CancellationToken,告知其应被取消]对象。这个类如下所示:
  public class CancellationTokenSource : IDisposable    {         //构造函数         public CancellationTokenSource();         //获取是否已请求取消此 System.Threading.CancellationTokenSource         public bool IsCancellationRequested { get; }         //获取与此 System.Threading.CancellationTokenSource 关联的 System.Threading.CancellationToken         public CancellationToken Token;         //传达取消请求。         public void Cancel();         //传达对取消的请求,并指定是否应处理其余回调和可取消操作。         public void Cancel(bool throwOnFirstException);        ... }
这个对象包含了管理取消有关的所有状态。构造好一个CancellationTokenSource(引用类型)之后,可以从它的Token属性获得一个或多个CancellationToken(值类型)实例,并传给你的操作,使那些操作可以取消。以下是CancellationToken值类型最有用的一些成员:
   public struct CancellationToken  //一个值类型    {        //获取此标记是否能处于已取消状态,IsCancellationRequested 由非通过Task来调用(invoke)的一个操作调用(call)        public bool IsCancellationRequested { get; }        //如果已请求取消此标记,则引发 System.OperationCanceledException,由通过Task来调用的操作调用        public void ThrowIfCancellationRequested();        //获取在取消标记时处于有信号状态的 System.Threading.WaitHandle,取消时,WaitHandle会收到信号        public WaitHandle WaitHandle { get; }        //返回空 CancellationToken 值。        public static CancellationToken None        //注册一个将在取消此 System.Threading.CancellationToken 时调用的委托。省略了简单重载版本        public CancellationTokenRegistration Register(Action<object> callback, object state, bool useSynchronizationContext);         //省略了GetHashCode、Equals成员    }
CancellationToken实例是一个轻量级的值类型,它包含单个私有字段:对它的CancellationTokenSource对象的一个引用。在一个计算限制操作的循环中,可以定时调用CancellationToken的IsCancellationRequested属性,了解循环是否应该提前终止,进而终止计算限制的操作。当然,提前终止的好处在于,CPU不再需要把时间浪费在你对其结果已经不感兴趣的一个操作上。现在,用一些示例代码演示一下:
  class Program    {        static void Main(string[] args)        {            CancellationTokenSource cts = new CancellationTokenSource();             // 将CancellationToken和"要循环到的目标数"传入操作中            ThreadPool.QueueUserWorkItem(o => Count(cts.Token, 1000));             Console.WriteLine("Press <Enter> to cancel the operation.");            Console.ReadLine();            cts.Cancel();  // 如果Count方法已返回,Cancel没有任何效果            // Cancel立即返回,方法从这里继续运行             Console.ReadLine();        }         private static void Count(CancellationToken token, Int32 countTo)        {            for (Int32 count = 0; count < countTo; count++)            {                //判断是否接收到了取消任务的信号                if (token.IsCancellationRequested)                {                    Console.WriteLine("Count is cancelled");                    break; // 退出循环以停止操作                }                 Console.WriteLine(count);
上一篇:执行CMD命令

下一篇:泛型总结

发表评论 共有条评论
用户名: 密码:
验证码: 匿名发表