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     }
线程池01

   如果回调方法有异常,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   }
ExecutionContext

可用这个类阻止一个执行上下文的流动,从而提升应用程序的性能对于服务器应用程序,性能的提升可能非常显著。但是,客户端应用程序的性能提升不了多少。另外,由于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提供的两个主要类型,它们是标准协作式取消模式的一部分。

      为了取消一个操作,首先必须创建一个System.Thread.CancellationTokenSource[通知 CancellationToken,告知其应被取消]对象。这个类如下所示:
  
 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

这个对象包含了管理取消有关的所有状态。构造好一个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

 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     }
取消示例
   注意:如果要执行一个操作,并禁止取消它,可以向该操作传递通过调用CancellationToken的静态None属性返回的CancellationToken
    如果愿意,可以登记一个或多个方法,在取消一个CancellationTokenSource时调用。每个回调方法都用CancellactionToken的Register方法来登记的。要向这个方法传递一个Action<Object>委托;一个要通过委托传给回调的状态;以及一个Boolean值(名为useSynchronizationContext),该值指定了是否要使用调用线程的SynchronizationContext来调用委托。如果为useSynchronizationContext参数传递的是false,那么调用Cancel的线程会顺序调用已登记的所有方法。如果为true,那么回调会被send(而不是post)给已捕捉的SynchronizationContext对象,后者决定由哪个线程调用回调方法。
    说明:如果执行send操作,要等到目标线程那里处理完毕之后才会返回。再次期间,调用线程会被阻塞。这相当于同步调用。而如果执行post操作,是指将东西post到一个队列中便完事,调用线程可以立即返回。相当于异步调用。以后会详细提到。
    如果多次调用Regiser,那么多个回调方法都会调用。这些回调方法可能抛出未处理的异常。如果调用CancellationTokenSource的Cancel方法,向它传递true,那么抛出了未处理异常的第一个回调方法会组织其他回调方法的执行,抛出的异常也会从Cancel中抛出。如果调用Cancel并向它传递false,那么登记的所有回调方法都会调用。所有未处理的异常都会添加到一个集合中。所有回调方法都执行后,如果其中任何一个抛出一个未处理的异常,Cancel就会排除一个AggregateException,该异常实例的InnerException属性会被设为以抛出的所有异常对象的一个集合。如果以等级的所有回调方法都没有抛出异常,那么Cancel直接返回,不抛出任何异常。
  注:没办法把InnerExceptions的一个异常与特定操作对应起来。需查看StackTrace属性并手动检查代码
    CancellactionToken的Register方法返回一个CancellationTokenRegistration,如下所示:
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来做相同的事情
    在上述代码中,创建了Task对象,并立即调用Start方法来调度该任务方法。当然,也可以先创建好Task对象,以后在调用Start方法。把Task对象传递给一个方法,由后者决定执行时间和频率
    创建一个Task的方式总是调用构造器,传递一个Action或者Action<Object>委托。这个委托就是你想执行的操作。如果传递期待一个Object的方法,还必须向Task的构造器传递最终想传给操作的实参。还可以选择向Task的构造器传递一个CancellationToken,这便允许Task在调度之前取消。
    还可以选择向构造器传递一些TaskCreationOptions标志来控制Task的执行方式。TaskCreationOptions是一个枚举类型,定义了一组可按位OR到一起的标志。它的定义如下:
 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     }
TaskCreationOptions

大多是标志只是一些提议而已,TaskScheduler在调度一个Task时,可能会也可能不会采纳这些提议。不过,AttacedToParent标志总是得到采纳,因为它和TaskScheduler本身无关。

   

  5.1 等待任务完成并获取它的结果

    对于任务,可以等待它完成,然后获取它们的结果。假定有一个Sum方法,在n值很大的时候,它要执行较长的时间:
    private static Int32 Sum(Int32 n) {
        Int32 sum = 0;
        for (; n > 0; n--) checked { sum += n; }    //如果n太大,这一行代码会抛出异常
        return sum;
    }
    现在可以构造一个Task<TResult>对象(派生自Task),并为泛型TResult参数传递计算限制操作的返回类型。在开始任务后,可以等待它完成并获取结果,如以下代码所示:
 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     }
Task01
如果计算限制的任务抛出一个未处理的异常,这个一样会被"侵吞"并存储到一个集合中,而线程池线程允许返回到线程池中。调用Wait方法或者Result属性时,这些成员会抛出一个System.AggregateException对象。
    提示:一个线程调用Wait方法时,系统会检查系统要等待的Task是否已开始执行。如果是,调用Wait的线程会阻塞,直到Task运行结束为止。但是,如果Task还没有开始执行,系统可能(取决于TaskSecheduler)使用调用Wait的线程来执行Task。如果发生这种情况,那么调用Wait的线程不会阻塞;它会执行Task并立即返回。这样做的好处在于,没有线程会被阻塞,所以减少了资源的使用(因为不需要创建一个线程来替代被阻塞的线程),并提升了性能(因为不需要花时间创建一个线程,也没有上下文切换)。但是不好的地方在于,加入线程在调用Wait前已经获得了一个线程同步锁,而Task试图获取同一个锁,就会造成一个死锁的线程。
    AggregateException类型用于封闭异常对象的一个集合(如果父任务生成了多个字任务,而多个子任务都抛出异常,这个集合便有可能包含多个异常)。该类型有一个InnerExceptions属性,它返回一个ReadOnlyCollection<Excepyion>对象。不要混淆InnerException和InnerException属性,后者是AggregateException类从System.Exception基类继承来的。对于上例来说,AggregateException的InnerExceptions属性的元素0将引用由计算限制方法(Sum)抛出的实际System.OverflowException对象。
    为方便编码,AggregateException重写了Exception的GetBaseException方法。AggregateException的这个实现会返回作为问题根源的最内层的AggregateException。AggregateException还提供了一个Flatten方法,它创建一个新的AggregateException,其InnerExceptions属性包含一个异常列表,其中的异常是通过遍历原始AggregateException的内层异常层次结构而生成的。最后,AggregateException还提供了一个Handle方法,它为AggregateException中包含的每个异常都调用一个回调方法,然后,回调方法可以为每个在调用Handle之后,如果至少有一个异常没有处理,就创建一个新的AggregateException对象,其中只包含未处理的异常,并抛出这个新的AggregateException对象。

     Task还提供等待任务数组。WaitAny是任意任务完成就返回,不再阻塞。反馈的是完成任务在数组中的下标,若超时则返回-1

    类似的,Task类还提供了静态WaitAll方法,它阻塞调用线程,直到数组中所有的Task对象都完成如果Task对象都完成,WaitAll方法返回true。如果​发生超时,就返回false

    如果WaitAnyWaitAll通过一个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;
   }

  

 在上述代码中,在计算限制的循环中,我们通过调用CancellationToken的ThrowIfCancellationRequested方法来定时检查操作是否已取消。这个方法和CancellationToken的IsCancellationRequested属性相似。如果CancellationTokenSource已经取消,ThrowIfCancellationRequested会抛出一个OpreationCanceledException。之所以选择抛出一个异常,是因为有别于ThreadPool的QueueUserWorkItem方法所初始化的工作项(work item),任务有表示已经完成的方法,甚至还有一个返回值。所以,需要采取一种方式将已完成的任务和出错的任务区分开。而让任务抛出一个异常,就可以知道任务没有一直运行到结束。
现在,我们像下面这样创建CancellationTokenSource和Task对象:
 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     }
可取消Task示例
 创建一个Task时,可以将一个CancellationToken传给Task的构造器,从而将这个CancellationToken和该Task关联起来。如果CancellationToken在Task调度前取消,Task会被取消,永远不会执行。但是,如果Task已经调度,那么Task为了允许它的操作在执行期间取消,Task的代码必须显式支持取消。遗憾的是,虽然Task对象关联了一个CancellationToken,但没有办法访问它。因此,必须通过某种方式,在Task的代码本身中获得用于创建Task对象的同一个CancellationToken。为了写这样的代码,最简单的方法就是使用一个lambda表达式,并将CancellationToken作为一个闭包变量"传递"(就像上例所示)。
 5.3 一个任务完成时自动启动一个新任务

     要写可伸缩的软件,一定不能使你的线程阻塞。这意味着如果调用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();
 
        }

  

  现在,当执行Sum的任务完成后,这个任务会启动另一个任务(也在某个线程池线程上)以显示结果。执行上述代码的线程不会进入阻塞状态并等待这个两个任务中的任何一个完成。相反,线程可以执行其它代码。如果线程线程本身就是线程池线程,它可以返回到池中,以执行其他操作。注意,执行Sum的任务可能在调用ContinueWith之前完成。但这不是一个问题,因为ContinueWith方法会看到Sum任务已经完成,会立即启动显示结果的任务。
    另外,注意ContinueWith会返回新的Task对象的一个引用。当然,可以用这个Task对象调用各种成员(比如Wait,Result,甚至ContinueWith),但你一般都是忽略这个Task对象,不把它的引用保存到一个变量中。
    Task对象内部包含了Continue任务的一个集合。所以,实际上可以用一个Task对象来多次调用ContinueWith。任务完成时,所有ContinueWith任务都会进入线程池的队列中。此外,调用ContinueWith时,可以传递对一组TaskContinuationOptions枚举值进行按位OR运行的结果。前4个标志(None,PreferFairness,LongRunning和AttachToParent)与早先描述的TaskCreationOptions枚举类型提供的标志完全一致,下面是TaskContinuationOptions类型的定义:
 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     }
TaskContinuationOptions

调用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();
 
        }
复制代码
    在前面例子中,父任务创建并启用3个Task对象。默认情况下,一个任务创建的Task对象是顶级任务,这些任务与创建它们的那个任务无关。然而,TaskContinuationOptions. AttachedToParent 标志将一个Task和创建它的那个Task关联起来,结果是除非所有子任务结束运行,否则创建任务(父任务)不会认为已经结束。调用ContinueWith方法创建一个Task时,可以指定TaskContinuationOptions. AttachedToParent 标志将延续任务指定的一个子任务
5.5 任务内部解密 
每个Task对象都有一组构成任务状态的字段。有一个Int32ID、代表Task执行状态的一个Int32、对父任务的一个引用、对Task创建时指定的TaskScheduler的一个引用、对回调方法的一个引用、对要传给回调方法的对象的一个引用(可通过Task的只读AsynState属性查询)、对一个ExecutionContext的引用以及对一个ManualResetEventSlim对象的引用。除此之外,每个Task对象都有对根据需要创建一个一些补充状态的一个引用。
    在补充状态中,包含一个CancellactionToken、一个ContinueWithTask对象集合、为抛出了未处理异常的子任务而准备的一个Task对象集合等。虽然任务提供了大量功能,但并非是没有代价的。因为必须为所有的这些状态分配内存。如果不需要任务提供的附加功能,那么使用ThreadPool.QueueUserWorkItem,资源的使用效率上会更高一些
    Task和Task<TResult>类实现了IDisposable接口,允许你在用完Task对象后调用Dispose。如今,所有Dispose方法所做的都是关闭ManuaResetEventSlim对象。然而,可以定义从Task和Task<Result>派生的类,在这些类中分配它们自己的资源,并在它们重写的Dispose方法中释放这些资源。当然,大多数开发人员都不会在自己的代码中显式的为一个Task对象调用Dispose;他们只让垃圾回收器回收任何不再需要的资源。
    在每个Yask对象中,都包含代表Task唯一ID的一个Int32字段。创建一个Task对象时,字段会被初始化为零。第一次查询Task的只读ID属性,属性将一个唯一Int32值分配给该字段,并从属性中返回它。TaskID从1开始,每分配一个ID都会递增1.在Visual Studio调试器中查看一个Task对象,会造成调试器显示Task的ID,从而造成为Task分配一个ID。
    这个ID的意义在于,每个Task都可以用一个唯一的值来标识。事实上,Visual Studio会在它的"并行任务"和"并行堆栈"窗口中会显示这个任务ID。但是,由于不在自己的代码中分配ID,所以几乎不可能将这个ID和代码正在做的事联系起来。运行一个任务的代码时,可以查询Task的静态CurrenId属性,它返回一个可空的Int32(Int32?)。还可以在调式期间,在Vasul Studio的"监视"或"即时"窗口中调用它,以便获得当前正在调试的代码的ID。然后,可以在"并行任务"和"并行堆栈"窗口中找到自己的任务。如果当前没有任务正在执行,查询CurrenId属性会返回null。
    一个Task对象存在期间,可查询Task的只读Status属性了解它在其生存期的什么位置。这个属性返回一个TaskStatus值,定义如下:
复制代码
public enum TaskStatus
  {
    //这些标志指出了一个Task在其生命周期内的状态
    // 任务已显式创建,可以手动Start()这个任务
    Created,
    // 任务已隐式创建,会自动开始
    WaitingForActivation,
    // 任务已调度,但尚未运行
    WaitingToRun,
    // 任务正在运行
    Running,
    // 任务正在等待它的子任务完成,子任务完成后它才完成
    WaitingForChildrenToComplete,
    // 一个任务的最终状态是以下三种之一
    // 已成功完成执行的任务
    RanToCompletion,
    // 该任务已通过对其自身的 CancellationToken 引发 OperationCanceledException 对取消进行了确认,此时该标记处于已发送信号状态;或者在该任务开始执行之前,已向该任务的 CancellationToken 发出了信号
    Canceled,
    // 由于未处理异常的原因而完成的任务
    Faulted
  }
复制代码
    首先构造一个Task对象时,它的状态是Created。以后,任务启动时,它的状态变为WaitngToRun。Task在一个线程上运行时,它的状态就变成了Running。任务停止运行,并等待它的任何子任务时,状态变成WaitingForChildrenToComplete。任务完全结束时,它会进入以下三种状态的一种:RanToCompletion、Canceled或Faulted。一个Task<Result>运行完成时,可通过Task<TResult>的Result属性来查询任务的结果。一个Task或者Task<TResult>出错时,可以查询Task的Exception属性来获得任务抛出的未处理的异常:该属性总是返回一个AggregateException对象,它包含了所有未处理的异常。
    为简化编码,Task提供了几个只读的Boolean属性:IsCanceled,IsFaulted和IsCompleted。注意,当Task处于RanToCompleted,Canceled或者Faulted状态时,IsCompleted返回true。为了判断一个Task是否成功完成,最简单的办法就是使用如下所示的代码:
if (task.Status == TaskStatus.RanToCompleted ).......
    如果Task是通过调用以下某个函数来创建的,这个Task对象就处于WaitingForActivation状态:ContinueWith、ContinueWithAll,ContinueWithAnv或者FromAsnc。如果通过构造一个TaskCompletionSource<TResult>对象[表示未绑定到委托的 Task<TResult> 的制造者方,并通过 Task 属性提供对使用者方的访问]创建一个Task,该Task在创建时也处于WaitingForActivation状态。这个状态意味着该Task的调度由任务基础结构控制。例如,不能显式启动一个通过ContinueWith创建的对象。这个Task会在它的先驱任务(antecedent task)执行完毕后自动开始。
5.6任务工厂  
     有的时候,可能需要创建一组Task任务来共享相同的状态。为了避免机械地将相同的参数传给每一个Task的构造器,可以创建一个任务工厂来封装通用的状态,System.Threding.Tasks命名空间定义了一个TaskFactory类型和一个TaskFactory<TResult>类型。两个类型都派生自System.Object;也就是说,它们是平级的。
    如果要创建的是一组没有返回值的任务,那么要构造一个TaskFactory;如果要创建的是一组有一个特定返回值的任务,那么要构造一个TaskFactory<TResult>,并通过泛型TResult实参来传递任务的返回类型。创建任何任务工厂类时,要向它的构造器传递一些默认值。工厂创建的任务都将具有这些默认值。具体的说,要想工厂传递你希望工厂创建的任务具有的CancellationToken,TaskScheduler,TaskCreationOptions和TaskContinuationOptions设置。
    以下实例代码演示了如何使用一个TaskFatory:
   
 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         }
TaskFactoryTest
 通过上述代码,创建了一个TaskFactory<Int32>对象。这个任务工厂将用于创建3个Task对象。希望它做4件事:每个Task对象都共享相同的CancellationTokenSource.Toke,其中3个任务被视为其父任务的子任务,TaskFactory对象创建的所有延续任务都同步执行,而且这个TaskFactory创建的所有Task对象都是用默认的TaskScheduler。
    然后创建一个数组,其中包含了3个子Task对象,所有都是通过TaskFactory的StartNew方法来创建的。使用这个方法,可以方便的创建并启动每个子任务。在一个循环中,告诉每个子任务,如果抛出一个未处理的异常,就会取消其它仍在运行的所有子任务。最后,在TaskFacroty上调用ContinueWithAll,它创建一个在所有子任务都结束后运行的一个Task。由于这个任务是用TaskFactory创建的,所以它仍然被视为父任务的一个子任务,会使用默认的TaskScheduler同步执行。然而,希望即使其他子任务被取消,也要运行这个任务。因此,我传递CancellationToken.None来覆盖TaskFactory的CancellationToken。这会造成该任务完全不能取消。最后,当处理所有结果的任务完成后,创建另一个任务来显示从所哟子任务中返回的最大值。
    注意:调用TaskFactory或TaskFactory<TRsult>的静态ContinueWhenAll和ContinueWhenAny方法时,以下TaskContinuationOption标志是非法的:NotOnRanToComplettion,NoyOnFaulted和NotCanceled。也就是说,无论先驱任务是如何完成的,ContinueWhenAll和ContinueWhenAny方法时都会执行延续任务
 
5.7  任务调度器(TaskScheduler) 
    任务基础结构是很灵活的,其中TaskScheduler对象功不可没。TaskScheduler对象负责执行调度的任务,同时向Visual Studio 调试器公开任务信息。FCL提供了两个派生自TaskScheduler的类型:线程池任务调度器(thread pool task scheduler)和同步上下文任务调度器(synchronization context task scheduler)。
    默认情况下,所有应用程序使用的都是线程池任务调度器。这个任务调度器将任务调度给线程池的工作者线程,将在后面进行更详细的讨论。可以查询TaskScheduler的静态Default属性来获得对默认任务调度器的一个引用。.
    同步上下文任务调度器通常用于Windows窗体、WPF和Silverlight应用程序。这个任务调度器将所有任务都调度给应用程序的GUI线程,是所有任务代码都能成功更新UI,比如按钮。菜单项等。同步上下文任务调度器根本不使用线程池。可以查询TaskScheduler的FromCurrentSynchronizationContext方法来获取对一个同步上下文任务调度器的引用。

~~~~~~待续。。。。

 

posted @ 2015-04-04 01:59  北落师门α  阅读(295)  评论(0编辑  收藏  举报