26计算限制的异步操作01-CLR
由CLR via C#(第三版) ,摘抄记录...
异步优点:在GUI应用程序中保持UI可响应性,以及多个CPU缩短一个耗时计算所需的时间。
1、CLR线程池基础:为提高性能,CLR包含了代码来管理他自己的线程池--线程的集合。每CLR一个线程池,这个线程池就由CLR控制的所有appDomain共享。如果你进程中有多个CLR,就有多个线程池。
CLR初始化时,池空,线程池维护一个操作请求队列。应用调用方法执行异步,将一个记录项(entry)追加到线程池的队列。线程池从队列提取记录项,派遣(dispatch)给一个线程池线程,如没有,则创建一个新线程。完成任务后线程不销毁,在线程池空闲等待响应另一个请求,这样提高性能。 当请求速度超过处理速度,就会创建额外线程。如果请求停止,线程空闲一段时间后,会自己醒来终止自己以释放资源。 线程池是启发式的,由任务多少,和可用CPU的多少,创建线程。
在内部,线程池将自己的线程分为 工作者(Worker)线程和I/0线程。
2、简单的计算限制操作
将一个异步的、计算限制的操作放到一个线程池的队列中,通常可以调用ThreadPool类定义的以下方法之一:
//将方法排入队列以便执行。此方法在有线程池线程变得可用时执行。 static Boolean QueueUserWorkItem(WaitCallback callBack); //将方法排入队列以便执行,并指定包含该方法所用数据的对象。此方法在有线程池线程变得可用时执行。 static Boolean QueueUserWorkItem(WaitCallback callBack,Object state);
~~~~
模拟程序
1 class Program 2 { 3 static void Main(string[] args) 4 { 5 Console.WriteLine("Main thread: queuing an asynchronous operation"); 6 ThreadPool.QueueUserWorkItem(ComputeBoundOp, 5); 7 Console.WriteLine("Main thread: Doing other work here..."); 8 Thread.Sleep(10000); // 模拟其它工作 (10 秒钟) 9 //Console.ReadLine(); 10 } 11 12 // 这是一个回调方法,必须和WaitCallBack委托签名一致 13 private static void ComputeBoundOp(Object state) 14 { 15 // 这个方法通过线程池中线程执行 16 Console.WriteLine("In ComputeBoundOp: state={0}", state); 17 Thread.Sleep(1000); // 模拟其它工作 (1 秒钟) 18 19 // 这个方法返回后,线程回到线程池,等待其他任务 20 } 21 }
如果回调方法有异常,CLR会终止进程。
3、 执行上下文 每个线程都关联了一个执行上下文数据结构。执行上下文(execution context)包括的东西有:
- 安全设置:压缩栈、Thread的Principal属性[指示线程的调度优先级]和Windows身份;
- 宿主设置:参见System.Threading.HostExecutionContextManager[提供使公共语言运行时宿主可以参与执行上下文的流动(或移植)的功能];
- 逻辑调用上下文数据:参见System.Runtime.Remoting.Messaging.CallContext[提供与执行代码路径一起传送的属性集]的LogicalSetData[将一个给定对象存储在逻辑调用上下文中并将该对象与指定名称相关联]和LogicalGetData[从逻辑调用上下文中检索具有指定名称的对象]。
线程执行代码时,有的操作会受到线程的执行上下文设置(尤其是安全设置)的影响。理想情况下,每当一个线程(初始线程)使用另一个线程(辅助线程)执行任务时,前者的执行上下文应该"流动"(复制)到辅助线程。这就确保辅助线程执行的任何操作使用的都是相同的安全设置和宿主设置。还确保了初始线程的逻辑调用上下文可以在辅助线程中使用。
默认情况下,CLR自动造成初始线程的执行上下文会"流动"(复制)到任何辅助线程。这就是将上下文信息传输到辅助线程,但这对损失性能,因为执行上下文中包含大量信息,而收集这些信息,再将这些信息复制到辅助线程,要耗费不少时间。如果辅助线程又采用更多的辅助线程,还必须创建和初始化更多的执行上下文数据结构。
System.Threading命名空间中有一个ExecutionContext类[管理当前线程的执行上下文],它允许你控制线程的执行上下文如何从一个线程"流动"(复制)到另一个线程。下面展示了这个类的样子:
1 public sealed class ExecutionContext : IDisposable, ISerializable 2 { 3 [SecurityCritical] 4 //取消执行上下文在异步线程之间的流动 5 public static AsyncFlowControl SuppressFlow(); 6 //恢复执行上下文在异步线程之间的流动 7 public static void RestoreFlow(); 8 //指示当前是否取消了执行上下文的流动。 9 public static bool IsFlowSuppressed(); 10 11 //不常用方法没有列出 12 }
可用这个类阻止一个执行上下文的流动,从而提升应用程序的性能。对于服务器应用程序,性能的提升可能非常显著。但是,客户端应用程序的性能提升不了多少。另外,由于SuppressFlow方法用[SecurityCritical]attribute进行了标识,所以在某些客户端应用程序(比如Silverlight)中是无法调用的。当然,只有在辅助线程不需要或者不防问上下文信息时,才应该组织执行上下文的流动。如果初始线程的执行上下文不流向辅助线程,辅助线程会使用和它关联起来的任何执行上下文。在这种情况下,辅助线程不应该执行要依赖于执行上下文状态(比如用户的Windows身份)的代码。
注意:添加到逻辑调用上下文的项必须是可序列化的。对于包含了逻辑调用上下文数据线的一个执行上下文,如果让它流动,可能严重损害性能,因为为了捕捉执行上下文,需对所有数据项进行序列化和反序列化。
下例展示了向CLR的线程池队列添加一个工作项的时候,如何通过阻止执行上下文的流动来影响线程逻辑调用上下文中的数据:
1 static void Main(string[] args) 2 { 3 // 将一些数据放到Main线程的逻辑调用上下文中 4 CallContext.LogicalSetData("Name", "Jeffrey"); 5 6 // 线程池能访问到逻辑调用上下文数据,加入到程序池队列中 7 ThreadPool.QueueUserWorkItem( 8 state => Console.WriteLine("Name={0}", CallContext.LogicalGetData("Name"))); 9 10 11 // 现在阻止Main线程的执行上下文流动 12 ExecutionContext.SuppressFlow(); 13 14 //再次访问逻辑调用上下文的数据 15 ThreadPool.QueueUserWorkItem( 16 state => Console.WriteLine("Name={0}", CallContext.LogicalGetData("Name"))); 17 18 //恢复Main线程的执行上下文流动 19 ExecutionContext.RestoreFlow(); 20 }
会得到一下结果:
Name=Jeffrey
Name=
虽然现在我们讨论的是调用ThreadPool.QueueUserWorkItem时阻止执行上下文的流动,但在使用Task对象(参见26.5节”任务“),以及在发起异步I/O操作(参见第27章“I/o限制的异步操作”)时,这个技术也会用到。
4、协作式取消 标准的取消模式,协作的,想取消的操作必须显式地支持取消。为长时间运行的的计算限制操作添加取消能力。
首先,先解释一下FCL提供的两个主要类型,它们是标准协作式取消模式的一部分。
1 public class CancellationTokenSource : IDisposable 2 { 3 //构造函数 4 public CancellationTokenSource(); 5 //获取是否已请求取消此 System.Threading.CancellationTokenSource 6 public bool IsCancellationRequested { get; } 7 //获取与此 System.Threading.CancellationTokenSource 关联的 System.Threading.CancellationToken 8 public CancellationToken Token; 9 //传达取消请求。 10 public void Cancel(); 11 //传达对取消的请求,并指定是否应处理其余回调和可取消操作。 12 public void Cancel(bool throwOnFirstException); 13 ... 14 }
这个对象包含了管理取消有关的所有状态。构造好一个CancellationTokenSource(引用类型)之后,可以从它的Token属性获得一个或多个CancellationToken(值类型)实例,并传给你的操作,使那些操作可以取消。以下是CancellationToken值类型最有用的一些成员:
1 public struct CancellationToken //一个值类型 2 { 3 //获取此标记是否能处于已取消状态,IsCancellationRequested 由非通过Task来调用(invoke)的一个操作调用(call) 4 public bool IsCancellationRequested { get; } 5 //如果已请求取消此标记,则引发 System.OperationCanceledException,由通过Task来调用的操作调用 6 public void ThrowIfCancellationRequested(); 7 //获取在取消标记时处于有信号状态的 System.Threading.WaitHandle,取消时,WaitHandle会收到信号 8 public WaitHandle WaitHandle { get; } 9 //返回空 CancellationToken 值。 10 public static CancellationToken None 11 //注册一个将在取消此 System.Threading.CancellationToken 时调用的委托。省略了简单重载版本 12 public CancellationTokenRegistration Register(Action<object> callback, object state, bool useSynchronizationContext); 13 14 //省略了GetHashCode、Equals成员 15 }
CancellationToken实例是一个轻量级的值类型,它包含单个私有字段:对它的CancellationTokenSource对象的一个引用。在一个计算限制操作的循环中,可以定时调用CancellationToken的IsCancellationRequested属性,了解循环是否应该提前终止,进而终止计算限制的操作。当然,提前终止的好处在于,CPU不再需要把时间浪费在你对其结果已经不感兴趣的一个操作上。现在,用一些示例代码演示一下:
1 class Program 2 { 3 static void Main(string[] args) 4 { 5 CancellationTokenSource cts = new CancellationTokenSource(); 6 7 // 将CancellationToken和"要循环到的目标数"传入操作中 8 ThreadPool.QueueUserWorkItem(o => Count(cts.Token, 1000)); 9 10 Console.WriteLine("Press <Enter> to cancel the operation."); 11 Console.ReadLine(); 12 cts.Cancel(); // 如果Count方法已返回,Cancel没有任何效果 13 // Cancel立即返回,方法从这里继续运行 14 15 Console.ReadLine(); 16 } 17 18 private static void Count(CancellationToken token, Int32 countTo) 19 { 20 for (Int32 count = 0; count < countTo; count++) 21 { 22 //判断是否接收到了取消任务的信号 23 if (token.IsCancellationRequested) 24 { 25 Console.WriteLine("Count is cancelled"); 26 break; // 退出循环以停止操作 27 } 28 29 Console.WriteLine(count); 30 Thread.Sleep(200); // 出于演示浪费一点时间 31 } 32 Console.WriteLine("Count is done"); 33 } 34 }
public struct CancellationTokenRegistration : IEquatable<CancellationTokenRegistration>, IDisposable { public void Dispose(); ....... }
可调用Dispose从关联的CancellationTokenSource中删除一个已登记的回调;这样一来,在调用Cancel时,便不会再调用这个回调。以下代码演示了如何向一个CancellationTokenSource登记两个回调:
private static void Register() { var cts = new CancellationTokenSource(); cts.Token.Register(() => Console.WriteLine("Canceled 1")); cts.Token.Register(() => Console.WriteLine("Canceled 2")); // 出于测试目的,让我们取消它,以便执行两个回调 cts.Cancel(); }
可通过链接另一组的CancellationTokenSource来新建一个CancellationTokenSource对象。任何一个链接的CancellationTokenSource被取消,这个CancellationTokenSource对象就会被取消。以下代码对此进行的演示:
1 class Program 2 { 3 static void Main(string[] args) 4 { 5 // 创建一个 CancellationTokenSource 6 var cts1 = new CancellationTokenSource(); 7 cts1.Token.Register(() => Console.WriteLine("cts1 canceled")); 8 9 // 创建另一个 CancellationTokenSource 10 var cts2 = new CancellationTokenSource(); 11 cts2.Token.Register(() => Console.WriteLine("cts2 canceled")); 12 13 // 创建新的CancellationTokenSource,它在 cts1 o或 ct2 is 取消时取消 14 var ctsLinked = CancellationTokenSource.CreateLinkedTokenSource(cts1.Token, cts2.Token); 15 ctsLinked.Token.Register(() => Console.WriteLine("linkedCts canceled")); 16 17 // 取消其中一个 CancellationTokenSource objects (这里选择了 cts2) 18 cts2.Cancel(); 19 20 // 显示哪个 CancellationTokenSource objects 被取消 了 21 Console.WriteLine("cts1 canceled={0}, cts2 canceled={1}, ctsLinked canceled ={2}", 22 cts1.IsCancellationRequested, cts2.IsCancellationRequested, ctsLinked.IsCancellationRequested); 23 24 Console.ReadLine(); 25 } 26 27 }
5、任务 调用ThreadPool的QueueUserWorkItem方法来发起一次异步的受计算限制的操作是非常简单的。然而。这个技术存在许多限制。最大的问题是没有一个内建的机制让你知道操作在什么时候完成,也没有一个机制在操作完成时获得一个返回值。为了克服这些限制并解决一些其它问题,Microsoft引入了任务(task)的概念。我们通过System.Treading.Tasks命名空间中的类型来使用它们。
ThreadPool.QueueUserWorkItem(ComputeBoundOp,5) // 调用QueueUserWorkItem new Task(ComputeBoundOp,5).Start(); // 用Task来做相同的事情
1 [FlagsAttribute, SerializableAttribute] 2 public enum TaskCreationOptions 3 { 4 //指定应使用默认行为 5 None = 0x0, 6 //提示 TaskScheduler 以一种尽可能公平的方式安排任务,这意味着较早安排的任务将更可能较早运行,而较晚安排运行的任务将更可能较晚运行。造成默认的TaskScheduler(任务调度器) 将线程池中的任务放到全局队列中,而不是放到一个工作者线程的本地队列中 7 PreferFairness = 0x1, 8 //指定某个任务将是运行时间长、粗粒度的操作。 它会给TaskScheduler一个提议,告诉它线程可能要“长时间运行”,将由TaskScheduler 决定如何解析还这个提示。 9 LongRunning = 0x2, 10 //将一个任务和它的父Task关联。 11 AttachedToParent = 0x4, 12 #if NET_4_5 13 // 14 DenyChildAttach = 0x8, 15 HideScheduler = 0x10 16 #endif 17 }
大多是标志只是一些提议而已,TaskScheduler在调度一个Task时,可能会也可能不会采纳这些提议。不过,AttacedToParent标志总是得到采纳,因为它和TaskScheduler本身无关。
5.1 等待任务完成并获取它的结果
private static Int32 Sum(Int32 n) { Int32 sum = 0; for (; n > 0; n--) checked { sum += n; } //如果n太大,这一行代码会抛出异常 return sum; }
1 class Program 2 { 3 static void Main(string[] args) 4 { 5 // 创建 Task, 推迟启动它 6 Task<Int32> t = new Task<Int32>(n => Sum((Int32)n), 10000); 7 8 // 可以在以后某个时间启动任务 9 t.Start(); 10 11 // 可以选择显式的等待任务完成 12 t.Wait(); 13 14 Console.WriteLine("The sum is: " + t.Result); //一个Int32的值 15 Console.ReadLine(); 16 } 17 18 private static Int32 Sum(Int32 n) 19 { 20 Int32 sum = 0; 21 for (; n > 0; n--) checked { sum += n; } //如果n太大,这一行代码会抛出异常 22 return sum; 23 } 24 25 }
Task还提供等待任务数组。WaitAny是任意任务完成就返回,不再阻塞。反馈的是完成任务在数组中的下标,若超时则返回-1。
类似的,Task类还提供了静态WaitAll方法,它阻塞调用线程,直到数组中所有的Task对象都完成。如果Task对象都完成,WaitAll方法返回true。如果发生超时,就返回false。
如果WaitAny、WaitAll通过一个CancellationToken而取消,会抛出一个OpreationCanceledException。
如果一直不调用 Wait获Result,或者一直不查询Task的Exception,就可能忽略一些异常,当Task对象的Finalize执行时,会抛出异常,这是CLR终结器抛出的,不能捕捉,进程会立即终止。(CLR第三版的 26.5.1章节提到一种处理,见P648)
5.2取消任务 可以用一个CancellationTokenSource取消一个Task.
private static Int32 Sum(CancellationToken ct, Int32 n) { Int32 sum = 0; for (; n > 0; n--) { // 在取消标志引用的CancellationTokenSource上如果调用Cancel, // 下面这一行就抛出OpreationCanceledException ct.ThrowIfCancellationRequested(); checked { sum += n; } //如果n太大,这一行代码会抛出异常 } return sum; }
1 static void Main(string[] args) 2 { 3 CancellationTokenSource cts = new CancellationTokenSource(); 4 Task<Int32> t = new Task<Int32>(() => Sum(cts.Token, 10000), cts.Token); 5 6 t.Start(); 7 8 // 在之后的某个时间,取消CancellationTokenSource以取消Task 9 cts.Cancel(); 10 11 try 12 { 13 // 如果任务已经取消,Result会抛出一个AggregateException 14 Console.WriteLine("The sum is: " + t.Result); // An Int32 value 15 } 16 catch (AggregateException ae) 17 { 18 // 将任何OperationCanceledException对象都视为已处理 19 // 其他任何异常都造成抛出一个新的AggregateException,其中 20 // 只包含未处理的异常 21 ae.Handle(e => e is OperationCanceledException); 22 23 // 所有的异常都处理好之后,执行下面这一行 24 Console.WriteLine("Sum was canceled"); 25 } 26 Console.ReadLine(); 27 }
要写可伸缩的软件,一定不能使你的线程阻塞。这意味着如果调用Wait,或者在任何尚未完成时查询任务的Result属性(Result内部会调用Wait),极有可能造成线程池创建一个新线程,这增大了资源的消耗,并损害了伸缩性。幸好,有更好的方式知道一个任务在上面时候结束运行。一个任务完成时,它可以启动另一个任务。下面重写了前面的代码,它不会阻塞线程:
static void Main(string[] args) { // 创建 Task, 推迟启动它, 继续另一个任务 Task<Int32> t = new Task<Int32>(n => Sum((Int32)n), 10000); // 可以在以后某个时间启动任务 t.Start(); // ContinueWith 返回一个 Task 但一般不再关心这个对象 Task cwt = t.ContinueWith(task => Console.WriteLine("The sum is: " + task.Result)); cwt.Wait(); Console.ReadLine(); }
1 [System.FlagsAttribute, System.SerializableAttribute] 2 public enum TaskContinuationOptions 3 { 4 None = 0x00000, 5 PreferFairness = 0x00001, 6 LongRunning = 0x00002, 7 AttachedToParent = 0x00004, 8 #if NET_4_5 9 DenyChildAttach = 0x00008, 10 HideScheduler = 0x00010, 11 LazyCancellation = 0x00020, 12 #endif 13 //指定不应在延续任务前面的任务已完成运行的情况下安排延续任务。 此选项对多任务延续无效。 14 NotOnRanToCompletion = 0x10000, 15 //指定不应在延续任务前面的任务引发了未处理异常的情况下安排延续任务。 此选项对多任务延续无效。 16 NotOnFaulted = 0x20000, 17 //指定不应在延续任务前面的任务已取消的情况下安排延续任务。 此选项对多任务延续无效。 18 NotOnCanceled = 0x40000, 19 //指定只应在延续任务前面的任务已完成运行的情况下才安排延续任务。 此选项对多任务延续无效。 20 OnlyOnRanToCompletion = 0x60000, 21 //指定只有在延续任务前面的任务引发了未处理异常的情况下才应安排延续任务。 此选项对多任务延续无效。 22 OnlyOnFaulted = 0x50000, 23 //指定只应在延续任务前面的任务已取消的情况下安排延续任务。此选项对多任务延续无效。 24 OnlyOnCanceled = 0x30000, 25 //指定应同步执行延续任务。 指定此选项后,延续任务将在导致前面的任务转换为其最终状态的相同线程上运行。 如果在创建延续任务时已经完成前面的任务,则延续任务将在创建此延续任务的线程上运行。 只应同步执行运行时间非常短的延续任务。 26 ExecuteSynchronously = 0x80000, 27 }
调用COntinueWith时,可以指定你希望新任务只有在第一个任务被取消时才运行,这时使用TaskContinuationOptions. OnlyOnCanceled标志来实现。默认情况下,如果没有指定上述任何标志,新任务无论如何都会执行下去,不管第一个任务是如何完成的。一个Task完成时,它的所有尚未运行的延续任务都会自动取消。下面用一个例子演示所有这些概念。
1 static void Main(string[] args) 2 { 3 Task<Int32> t = new Task<Int32>(n => Sum((Int32)n), 10000); 4 5 t.Start(); 6 7 // 每个 ContinueWith 都返回一个 Task,但你不必关心这些Task对象 8 t.ContinueWith(task => Console.WriteLine("The sum is: " + task.Result), 9 TaskContinuationOptions.OnlyOnRanToCompletion); 10 t.ContinueWith(task => Console.WriteLine("Sum threw: " + task.Exception), 11 TaskContinuationOptions.OnlyOnFaulted); 12 t.ContinueWith(task => Console.WriteLine("Sum was canceled"), 13 TaskContinuationOptions.OnlyOnCanceled); 14 15 Console.ReadLine(); 16 17 }
5.4任务启动子任务
static void Main(string[] args) { Task<Int32[]> parent = new Task<Int32[]>(() => { var results = new Int32[3]; // 创建数组来存储结果 // 这个任务创建并启用了3个子任务 new Task(() => results[0] = Sum(10000), TaskCreationOptions.AttachedToParent).Start(); new Task(() => results[1] = Sum(20000), TaskCreationOptions.AttachedToParent).Start(); new Task(() => results[2] = Sum(30000), TaskCreationOptions.AttachedToParent).Start(); // 返回对数组的一个引用(即使数组元素可能还没有初始化) return results; }); var cwt = parent.ContinueWith(parentTask => Array.ForEach(parentTask.Result, Console.WriteLine)); parent.Start(); Console.ReadLine(); }
public enum TaskStatus { //这些标志指出了一个Task在其生命周期内的状态 // 任务已显式创建,可以手动Start()这个任务 Created, // 任务已隐式创建,会自动开始 WaitingForActivation, // 任务已调度,但尚未运行 WaitingToRun, // 任务正在运行 Running, // 任务正在等待它的子任务完成,子任务完成后它才完成 WaitingForChildrenToComplete, // 一个任务的最终状态是以下三种之一 // 已成功完成执行的任务 RanToCompletion, // 该任务已通过对其自身的 CancellationToken 引发 OperationCanceledException 对取消进行了确认,此时该标记处于已发送信号状态;或者在该任务开始执行之前,已向该任务的 CancellationToken 发出了信号 Canceled, // 由于未处理异常的原因而完成的任务 Faulted }
if (task.Status == TaskStatus.RanToCompleted ).......
1 private static void Test5() 2 { 3 var parent = new Task(() => 4 { 5 var cts = new CancellationTokenSource(); 6 var tf = new TaskFactory<Int32>(cts.Token, TaskCreationOptions.AttachedToParent, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default); 7 8 // 这个任务创建并启动三个子任务 9 var childTasks = new[] { 10 tf.StartNew(() => Sum(cts.Token, 10000)), 11 tf.StartNew(() => Sum(cts.Token, 20000)), 12 tf.StartNew(() => Sum(cts.Token, Int32.MaxValue)) , // 太大,抛出 OverflowException异常 13 tf.StartNew(() => Sum(cts.Token, 30000)) 14 }; 15 16 // 如果子任务抛出异常就取消其余子任务 17 for (Int32 task = 0; task < childTasks.Length; task++) 18 childTasks[task].ContinueWith(t => cts.Cancel(), TaskContinuationOptions.OnlyOnFaulted); 19 20 // 所有子任务完成后,从未出错/未取消的任务返回的值, 21 // 然后将最大值传给另一个任务来显示结果 22 tf.ContinueWhenAll(childTasks, 23 completedTasks => completedTasks.Where(t => !t.IsFaulted && !t.IsCanceled).Max(t => t.Result), 24 CancellationToken.None) 25 .ContinueWith(t => Console.WriteLine("The maximum is: " + t.Result), 26 TaskContinuationOptions.ExecuteSynchronously); 27 }); 28 // 子任务完成后,也显示任何未处理的异常 29 parent.ContinueWith(p => 30 { 31 // 将所有文本放到一个 StringBuilder 中并只调用 Console.WrteLine 一次 32 // 因为这个任务可能和上面任务并行执行,而我不希望任务的输出变得不连续 33 StringBuilder sb = new StringBuilder("The following exception(s) occurred:" + Environment.NewLine); 34 foreach (var e in p.Exception.Flatten().InnerExceptions) 35 sb.AppendLine(" " + e.GetType().ToString()); 36 Console.WriteLine(sb.ToString()); 37 }, TaskContinuationOptions.OnlyOnFaulted); 38 39 // 启动父任务,便于它启动子任务 40 parent.Start(); 41 }
~~~~~~待续。。。。
结语:我只想把我所知道的,尽量简洁清楚地表达出来。