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

简介: 原文:[CLR via C#]26. 计算限制的异步操作  一、CLR线程池基础     前面说过,创建和销毁线程是一个比较昂贵的操作,太多的线程也会浪费内存资源。由于操作系统必须调度可运行的线程并执行上下文切换,所以太多的线程还有损于性能。
原文: [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 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调度器决定先调度哪一个线程。
 
  三、执行上下文
 
    每个线程都关联了一个执行上下文数据结构。执行上下文(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=Jeffrey
Name=
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);
                Thread.Sleep(200);   // 出于演示浪费一点时间
            }
            Console.WriteLine("Count is done");
        }
    }
 
    注意:如果要执行一个操作,并禁止取消它,可以向该操作传递通过调用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直接返回,不抛出任何异常。
    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();
}
    输出结果为:
Canceled 2
Canceled 1
     最后,可通过链接另一组的CancellationTokenSource来新建一个CancellationTokenSource对象。任何一个链接的CancellationTokenSource被取消,这个CancellationTokenSource对象就会被取消。以下代码对此进行的演示:
class Program
    {
        static void Main(string[] args)
        {
            // 创建一个 CancellationTokenSource
            var cts1 = new CancellationTokenSource();
            cts1.Token.Register(() => Console.WriteLine("cts1 canceled"));
 
            // 创建另一个 CancellationTokenSource
            var cts2 = new CancellationTokenSource();
            cts2.Token.Register(() => Console.WriteLine("cts2 canceled"));
 
            // 创建新的CancellationTokenSource,它在 cts1 o或 ct2 is 取消时取消
            var ctsLinked = CancellationTokenSource.CreateLinkedTokenSource(cts1.Token, cts2.Token);
            ctsLinked.Token.Register(() => Console.WriteLine("linkedCts canceled"));
 
            // 取消其中一个 CancellationTokenSource objects (这里选择了 cts2)
            cts2.Cancel();
 
            // 显示哪个 CancellationTokenSource objects 被取消 了
            Console.WriteLine("cts1 canceled={0}, cts2 canceled={1}, ctsLinked canceled ={2}",
               cts1.IsCancellationRequested, cts2.IsCancellationRequested, ctsLinked.IsCancellationRequested);
 
            Console.ReadLine();
        }
 
    }
    输出结果:
linkedCts canceled
cts2 canceled
cts1 canceled=False, cts2 canceled=True, ctsLinked canceled =True
 
  五、任务
 
    调用ThreadPool的QueueUserWorkItem方法来发起一次异步的受计算限制的操作是非常简单的。然而。这个技术存在许多限制。最大的问题是没有一个内建的机制让你知道操作在什么时候完成,也没有一个机制在操作完成时获得一个返回值。为了克服这些限制并解决一些其它问题,Microsoft引入了任务(task)的概念。我们通过System.Treading.Tasks命名空间中的类型来使用它们。
    现在,我们可以使用任务来做相同的事情:
    ThreadPool.QueueUserWorkItem(ComputeBoundOp,5)    // 调用QueueUserWorkItem
    new Task(ComputeBoundOp,5).Start();                                // 用Task来做相同的事情
    在上述代码中,创建了Task对象,并立即调用Start方法来调度该任务方法。当然,也可以先创建好Task对象,以后在调用Start方法。
    创建一个Task的方式总是调用构造器,传递一个Action或者Action<Object>委托。这个委托就是你想执行的操作。如果传递期待一个Object的方法,还必须向Task的构造器传递最终想传给操作的实参。还可以选择向Task的构造器传递一个CancellationToken,这便允许Task在调度之前取消。
    还可以选择向构造器传递一些TaskCreationOptions标志来控制Task的执行方式。TaskCreationOptions是一个枚举类型,定义了一组可按位OR到一起的标志。它的定义如下:
[FlagsAttribute, SerializableAttribute]
    public enum TaskCreationOptions
    {
        //指定应使用默认行为
        None             = 0x0,
        //提示 TaskScheduler 以一种尽可能公平的方式安排任务,这意味着较早安排的任务将更可能较早运行,而较晚安排运行的任务将更可能较晚运行。造成默认的TaskScheduler(任务调度器) 将线程池中的任务放到全局队列中,而不是放到一个工作者线程的本地队列中
        PreferFairness   = 0x1,
        //指定某个任务将是运行时间长、粗粒度的操作。 它会给TaskScheduler一个提议,告诉它线程可能要“长时间运行”,将由TaskScheduler 决定如何解析还这个提示。
        LongRunning      = 0x2,
        //将一个任务和它的父Task关联。
        AttachedToParent = 0x4,
#if NET_4_5
        //
        DenyChildAttach  = 0x8,
        HideScheduler    = 0x10
#endif
    }

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

    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参数传递计算限制操作的返回类型。在开始任务后,可以等待它完成并获取结果,如以下代码所示:
class Program
    {
        static void Main(string[] args)
        {
           // 创建 Task, 推迟启动它
            Task<Int32> t = new Task<Int32>(n => Sum((Int32)n), 10000);
 
            // 可以在以后某个时间启动任务
            t.Start();
 
            // 可以选择显式的等待任务完成
            t.Wait();
 
            Console.WriteLine("The sum is: " + t.Result);   //一个Int32的值
            Console.ReadLine();
        }
 
        private static Int32 Sum(Int32 n)
        {
            Int32 sum = 0;
            for (; n > 0; n--) checked { sum += n; }    //如果n太大,这一行代码会抛出异常
            return sum;
        }
 
    }
    如果计算限制的任务抛出一个未处理的异常,这个一样会被"侵吞"并存储到一个集合中,而线程池线程允许返回到线程池中。调用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类还提供了静态WaitAll方法,它阻塞调用线程,直到数组中所有的Task对象都完成。如果Task对象都完成,WaitAll方法返回true。如果​发生超时,就返回false。如果WaitAll通过一个CancellationToken而取消,会抛出一个OpreationCanceledException。

    2、取消任务

    可以用一个CancellationTokenSource取消一个Task。首先,我们必须修订签名的Sum方法,让它接受一个CancellationToken:
      
  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对象:
    static void Main(string[] args)
        {
            CancellationTokenSource cts = new CancellationTokenSource();
            Task<Int32> t = new Task<Int32>(() => Sum(cts.Token, 10000), cts.Token);
 
            t.Start();
 
            // 在之后的某个时间,取消CancellationTokenSource以取消Task
            cts.Cancel();
 
            try
            {
                // 如果任务已经取消,Result会抛出一个AggregateException
                Console.WriteLine("The sum is: " + t.Result);   // An Int32 value
            }
            catch (AggregateException ae)
            {
                // 将任何OperationCanceledException对象都视为已处理
                // 其他任何异常都造成抛出一个新的AggregateException,其中
                // 只包含未处理的异常      
                ae.Handle(e => e is OperationCanceledException);
 
                // 所有的异常都处理好之后,执行下面这一行
                Console.WriteLine("Sum was canceled");
            }
            Console.ReadLine();
    }
    创建一个Task时,可以将一个CancellationToken传给Task的构造器,从而将这个CancellationToken和该Task关联起来。如果CancellationToken在Task调度前取消,Task会被取消,永远不会执行。但是,如果Task已经调度,那么Task为了允许它的操作在执行期间取消,Task的代码必须显式支持取消。遗憾的是,虽然Task对象关联了一个CancellationToken,但没有办法访问它。因此,必须通过某种方式,在Task的代码本身中获得用于创建Task对象的同一个CancellationToken。为了写这样的代码,最简单的方法就是使用一个lambda表达式,并将CancellationToken作为一个闭包变量"传递"(就像上例所示)。
 
    3、一个任务完成时自动启动一个新任务
 
    要写可伸缩的软件,一定不能使你的线程阻塞。这意味着如果调用Wait,或者在任何尚未完成时查询任务的Result属性,极有可能造成线程池创建一个新线程,这增大了资源的消耗,并损害了伸缩性。幸好,有更好的方式知道一个任务在上面时候结束运行。一个任务完成时,它可以启动另一个任务。下面重写了前面的代码,它不会阻塞线程:
 
    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类型的定义:
[System.FlagsAttribute, System.SerializableAttribute]
    public enum TaskContinuationOptions
    {
        None                  = 0x00000,
        PreferFairness        = 0x00001,
        LongRunning           = 0x00002,
        AttachedToParent      = 0x00004,
#if NET_4_5
        DenyChildAttach       = 0x00008,
        HideScheduler         = 0x00010,
        LazyCancellation      = 0x00020,
#endif
        //指定不应在延续任务前面的任务已完成运行的情况下安排延续任务。 此选项对多任务延续无效。
        NotOnRanToCompletion  = 0x10000,
        //指定不应在延续任务前面的任务引发了未处理异常的情况下安排延续任务。 此选项对多任务延续无效。
        NotOnFaulted          = 0x20000,
        //指定不应在延续任务前面的任务已取消的情况下安排延续任务。 此选项对多任务延续无效。
        NotOnCanceled         = 0x40000,
        //指定只应在延续任务前面的任务已完成运行的情况下才安排延续任务。 此选项对多任务延续无效。
        OnlyOnRanToCompletion = 0x60000,
        //指定只有在延续任务前面的任务引发了未处理异常的情况下才应安排延续任务。 此选项对多任务延续无效。
        OnlyOnFaulted         = 0x50000,
        //指定只应在延续任务前面的任务已取消的情况下安排延续任务。此选项对多任务延续无效。
        OnlyOnCanceled        = 0x30000,
        //指定应同步执行延续任务。 指定此选项后,延续任务将在导致前面的任务转换为其最终状态的相同线程上运行。 如果在创建延续任务时已经完成前面的任务,则延续任务将在创建此延续任务的线程上运行。 只应同步执行运行时间非常短的延续任务。
        ExecuteSynchronously  = 0x80000,
    }
    调用COntinueWith时,可以指定你希望新任务只有在第一个任务被取消时才运行,这是使用TaskContinuationOptions. OnlyOnCanceled标志来实现。默认情况下,如果没有指定上述任何标志,新任务无论如何都会执行下去,不管第一个任务是如何完成的。一个Task完成时,它的所有尚未运行的延续任务都会自动取消。下面用一个例子演示所有这些概念。
static void Main(string[] args)
        {
            Task<Int32> t = new Task<Int32>(n => Sum((Int32)n), 10000);
 
            t.Start();
 
            // 每个 ContinueWith 都返回一个 Task,但你不必关心这些Task对象
            t.ContinueWith(task => Console.WriteLine("The sum is: " + task.Result),
               TaskContinuationOptions.OnlyOnRanToCompletion);
            t.ContinueWith(task => Console.WriteLine("Sum threw: " + task.Exception),
               TaskContinuationOptions.OnlyOnFaulted);
            t.ContinueWith(task => Console.WriteLine("Sum was canceled"),
               TaskContinuationOptions.OnlyOnCanceled);
 
            Console.ReadLine();
 
        }
 
    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、任务内部揭秘
 
    每个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)执行完毕后自动开始。
 
    6、任务工厂
    
    有的时候,可能需要创建一组Task任务来共享相同的状态。为了避免机械地将相同的参数传给每一个Task的构造器,可以创建一个任务工厂来封装通用的状态,System.Threding.Tasks命名空间定义了一个TaskFactory类型和一个TaskFactory<TResult>类型。两个类型都派生自System.Object;也就是说,它们是平级的。
    如果要创建的是一组没有返回值的任务,那么要构造一个TaskFactory;如果要创建的是一组有一个特定返回值的任务,那么要构造一个TaskFactory<TResult>,并通过泛型TResult实参来传递任务的返回类型。创建任何任务工厂类时,要向它的构造器传递一些默认值。工厂创建的任务都将具有这些默认值。具体的说,要想工厂传递你希望工厂创建的任务具有的CancellationToken,TaskScheduler,TaskCreationOptions和TaskContinuationOptions设置。
    以下实例代码演示了如何使用一个TaskFatory:
var cts = new CancellationTokenSource();
                var tf = new TaskFactory<Int32>(cts.Token, TaskCreationOptions.AttachedToParent, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default);
 
                // 这个任务创建并启动三个子任务
                var childTasks = new[] {
                    tf.StartNew(() => Sum(cts.Token, 10000)),
                    tf.StartNew(() => Sum(cts.Token, 20000))
                    tf.StartNew(() => Sum(cts.Token, Int32.MaxValue))  // 太大,抛出 OverflowException异常
                };
 
                // 如果子任务抛出异常蛮久取消其余子任务
                for (Int32 task = 0; task < childTasks.Length; task++)
                    childTasks[task].ContinueWith(t => cts.Cancel(), TaskContinuationOptions.OnlyOnFaulted);
 
                // 所有子任务完成后,从未出错/未取消的任务返回的值,
                // 然后将最大值传给另一个任务来显示结果
                tf.ContinueWhenAll(childTasks,
                   completedTasks => completedTasks.Where(t => !t.IsFaulted && !t.IsCanceled).Max(t => t.Result),
                   CancellationToken.None)
                   .ContinueWith(t => Console.WriteLine("The maximum is: " + t.Result),
                      TaskContinuationOptions.ExecuteSynchronously).Wait();
            });
 
            // 子任务完成后,也显示任何未处理的异常
            parent.ContinueWith(p =>
            {
                // 将所有文本放到一个 StringBuilder 中并只调用 Console.WrteLine 一次
                // 因为这个任务可能和上面任务并行执行,而我不希望任务的输出变得不连续
                StringBuilder sb = new StringBuilder("The following exception(s) occurred:" + Environment.NewLine);
                foreach (var e in p.Exception.Flatten().InnerExceptions)
                    sb.AppendLine("   " + e.GetType().ToString());
                Console.WriteLine(sb.ToString());
            }, TaskContinuationOptions.OnlyOnFaulted);
 
            // 启动父任务,便于它启动子任务
            parent.Start();
    
    通过上述代码,创建了一个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方法时都会执行延续任务。
 
    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方法来获取对一个同步上下文任务调度器的引用。
 
  六、Parallel的静态For,ForEach和Invoke方法
 
    在一些常见的编程情形中,使用任务也许会提升性能。为了简化编程,静态类System.Threading.Tasks.Paraller封装了这些常见的情形,它内部使用Task对象。例如,不要像下面一样处理一个集合中的所有项:
// 一个线程顺序执行这个工作(每次迭代调用一次DoWork)
for (Int32 i = 0; i< 1000; i++ ) DoWork(i);

    相反,可以使用Parallel类型的For方法,让多个线程池治线程帮助执行这个工作:

// 线程池的线程并行处理工作
Parallel.For(0,1000,i=>DoWork(i));

    类似的,如果有一个集合,那么不要像下面这样写:

// 一个线程顺序执行这个工作(每次迭代调用一次DoWork)
foreach ( var item in conllection) DoWork(item);

    而是这样做:

// 线程池的线程并行处理工作
Parallel.ForEach(conllection,item=>DoWork(item));

    如果代码中既可以用For,也可以用ForEach,那么建议使用For,因为它执行的快一点。最后,如果要执行几个方法,那么可以顺序执行它们,如下所示:

//  一个线程顺序执行所有方法    
Method1();
Method2();
Method3();

    也可以并行执行它们: 

// 线程池的线程并行执行
parallel.Invoke(
            () => Method1(),
            () => Method2(),
            () => Method3());
 

    Parallel的所有方法都让调用线程参与处理。从资源利用的角度说,这是一件好事,因为我们不希望调用线程停下来,等待线程池做完所有工作后才继续。然而,如果调用线程在线程池完成自己的那一部分工作之前完成工作,调用程序就会将自己挂起,知道所有工作完成。这也是一件好事,因为这个提供了和普通for和foreach循环时相同的语义:线程要在所有工作后才继续运行。还要注意,如果任何操作抛出一个未处理的异常,你调用的paraller方法最后会抛出一个AggregateException。

    当然,这并不是说需要检查自己的所有源代码,将for循环替换成Parallel.For的调用。调用Parallel的方法时,有一个前提条件务必记住:工作项要能并行执行。因此,如果工作项必须顺序执行,就不要调用Parallel的方法。另外,要避免会修改任何共享数据的工作项,因为多个线程同时处理的数据可能损坏。为了解决这个问题,一般的方法就是围绕数据访问添加线程同步锁。但是这样一来,一次就只能有一个线程访问数据,无法享受并行处理多个想带来的好处。
    除此之外,Parallel的方法本身也有开销:委托对象必须分配,而针对每一个工作项,都要调用一次这些委托。如果有大量可由多个线程处理的工作项,那么也许会获得性能的提升。但是,如果只为区区几个工作项使用Parallel的方法,或者为处理得非常快的工作项使用Parallel就会得不偿失了。
    Parallel的For,ForEach和Invoke方法都能接受一个ParallelOptions对象的重载版本。这个对象的定义如下:
   
 // 存储用于配置 Parallel 类的方法的操作的选项。
     public class ParallelOptions
    {
        // 初始化 ParallelOptions 类的新实例
        public ParallelOptions();
        // 获取或设置与此 ParallelOptions 实例关联的 CancellationToken,运行取消操作
        public CancellationToken CancellationToken { get; set; }
        // 获取或设置此 ParallelOptions 实例所允许的最大并行度,默认为-1(可用CPU数)
        public int MaxDegreeOfParallelism { get; set; }
        // 获取或设置与此 ParallelOptions 实例关联的 TaskScheduler。默认为TaskScheduler.Default
        public TaskScheduler TaskScheduler { get; set; }
    }

    除此之外,For和ForEach方法有一些重载版本允许传递3个委托:

    任务局部初始化委托(localInit),为参与工作的每一个任务都调用一次委托。这个委托是在任务被要求处理一个工作项之前调用。
    主体委托(body),为参与工作的各个线程所处理的每一项都调用一次委托。
    任务局部终结委托(localFinally),为参与工作的每一个任务都调用一次委托。这个委托是在任务处理好派遣给它的所有工作之后调用。即使主体委托引发一个未处理的异常,也会调用它。
    以下示例代码演示了如何利用3个委托,计算一个目录中的所有文件的字节长度总计值:
private static Int64 DirectoryBytes(String path, String searchPattern, SearchOption searchOption)
        {
            var files = Directory.EnumerateFiles(path, searchPattern, searchOption);
            Int64 masterTotal = 0;
 
            ParallelLoopResult result = Parallel.ForEach<String, Int64>(files,
               () =>
               {
                   // localInit: 每个任务开始之前调用一次
                   // 每个任务开始之前,总计值都初始化为0
                   return 0;
               },
 
               (file, parallelLoopState, index, taskLocalTotal) =>
               {
                   // body: 每个任务调用一次
                   // 获得这个文件的大小,把它添加到这个任务的累加值上
                   Int64 fileLength = 0;
                   FileStream fs = null;
                   try
                   {
                       fs = File.OpenRead(file);
                       fileLength = fs.Length;
                   }
                   catch (IOException) { /* 忽略拒绝访问的文件 */ }
                   finally { if (fs != null) fs.Dispose(); }
                   return taskLocalTotal + fileLength;
               },
 
               taskLocalTotal =>
               {
                   // localFinally: 每个任务完成后调用一次
                   // 将这个任务的总计值(taskLocalTotal)加到中的总计值(masterTotal)上去
                   Interlocked.Add(ref masterTotal, taskLocalTotal);
               });
            return masterTotal;
        }

    每个任务都通过taskLocalTotal变量为分配给它的文件维护自己的总计值。每个任务完成工作之后,都调用Interlocked.Add方法[对两个 32 位整数进行求和并用和替换第一个整数],以一种线程安全的方式更新总的总计值。由于每个任务都有自己的总计值,可以在一个工作项处理期间,无需进行线程同步。由于线程同步会造成性能的损失,所以不需要线程同步是一件好事。只有在每个任务返回之后,masterTotal才需要以一种线程安全的方式更新materTotal变量。所以,因为调用Interlocked.Add方法而造成的性能损失每个任务只发生一次,而不会每个工作项都发生。

    注意,我们向主题委托传递一个ParallelLoopState对象,它的定义如下:
    
// 可用来使 Parallel 循环的迭代与其他迭代交互
    public class ParallelLoopState
    {
        // 获取循环的任何迭代是否已引发相应迭代未处理的异常
        public bool IsExceptional { get; }
        // 获取循环的任何迭代是否已调用 Stop
        public bool IsStopped { get; }
        // 获取从中调用 Break 的最低循环迭代。
        public long? LowestBreakIteration { get; }
        // 获取循环的当前迭代是否应基于此迭代或其他迭代发出的请求退出。
        public bool ShouldExitCurrentIteration { get; }
        // 告知 Parallel 循环应在系统方便的时候尽早停止执行当前迭代之外的迭代。
        public void Break();
        // 告知 Parallel 循环应在系统方便的时候尽早停止执行。
        public void Stop();
}

      参与工作的每一个任务都会获得它自己的ParallelState对象,并可通过这个对象和参与工作的其他任务进行交互。Stop方法告诉循环停止处理任何更多的工作,未来对IsStopped属性的查询会返回true。Break方法告诉循环不再继续处理当前项之后的项。例如,假如ForEach被告知要处理100项,并在第5项时调用了Break,那么循环会确保前5项处理好之后,ForEach才返回。但注意,这并不是说在这100项中,只有前5项被处理,也许第5项之后可能在以前已经处理过了。LowestBreakIteration属性返回在处理过程中调用过Break方法的最低的项。从来没有调用过Break,LowestBreakIteration会返回null。

    处理任何一项时,如果造成一个未处理的异常,IsExceptional属性会返回true。如果处理一项时会花费大量的时间,代码可查询ShouldExitCurrentIteration属性看它是否应该提前退出。如果调用过Stop,调用过Break,取消过CancellationTokenSource,或者处理一项时造成了未处理的异常,这个属性就会返回true。
    Parallel的For和ForEach方法都返回一个ParallelLoopResult实例,他看起来像下面这样:
   
// 提供执行 System.Threading.Tasks.Parallel 循环的完成状态。
    public struct ParallelLoopResult
    {
        // 获取该循环是否已运行完成(即该循环的所有迭代均已执行,并且该循环没有收到提前结束的请求)。
        public bool IsCompleted { get; }
 
        // 获取从中调用 System.Threading.Tasks.ParallelLoopState.Break() 的最低迭代的索引。
        public long? LowestBreakIteration { get; }
    }

    可通过检查属性来了解循环的结果,如果IsCompleted返回true。表明循环运行完成,所有项都得到了处理。如果IsCompleted为false,而且LowestBreakIteration为null,表明参与工作的某个线程调用了Stop方法。如果LowestBreakIteration返回false,而且LowestBreakIteration不为null,表名参与工作的某个线程调用的Break方法,LowestBreakIteration返回的Int64值指明了保证已得到处理的最低一项的索引。

 

   七、 并行语言查询(PLINQ)
    Microsoft的语言集成查询(LINQ)功能提供了一个简捷的语法来查询数据集合。使用LINQ,可轻松对数据线进行筛选、排序、投射等。使用LINQ to Object时,只有一个线程顺序处理数据集合中的所有项;我们称为顺序查询。为了提高处理性能,可以使用并行LINQ,它将顺序查询转换成为一个并行查询,在内部使用任务(这些任务的排列由默认TaskScheduler来调度),将集合中的数据线的处理工作分散到多个CPU上,一边并发处理多个数据。和Prarllel的方法相似,如果同时要处理大量项,或者每一项的处理过程都是一个耗时的计算限制的操作,那么就可以使用PLINQ获得最大的收益。
    静态System.Linq.ParallelEnumerable类(在System.Core.dll中定义)实现了PLINQ的所有功能,所以必须通过C#的using指令将System.Linq命名空间导入到你的源码中。尤其是,这个类公开了所有标准LINQ操作符的并行版本,比如Where,Select,SelectMany,GroupBy,Join,Skip,Task等。所有这些方法都是扩展了System.Linq.ParallelQuery<T>类型的扩展方法。为了让自己的LINQ to Object查询调用这些方法的并行版本,必须将自己的顺序查询(基于IEnumberable或者IEnumerable<T>)转换成并行查询(基于ParallelQuery或者ParallelQuery<T>),这是用ParallelEnumerable的AsParallel扩展方法来实现的,如下所示:
public static ParallelQuery<TSource> AsParallel<TSource>(this IEnumerable<TSource> source)

    下面是将一个顺序查询转换成并行查询的例子。查询返回的是一个程序集中定义的所有过时(obsolete)方法。

private static void ObsoleteMethods(Assembly assembly)
        {
            var query =
               from type in assembly.GetExportedTypes().AsParallel()
               from method in type.GetMethods(BindingFlags.Public |
                  BindingFlags.Instance | BindingFlags.Static)
               let obsoleteAttrType = typeof(ObsoleteAttribute)
               where Attribute.IsDefined(method, obsoleteAttrType)
               orderby type.FullName
               let obsoleteAttrObj = (ObsoleteAttribute)
                  Attribute.GetCustomAttribute(method, obsoleteAttrType)
               select String.Format("Type={0}\nMethod={1}\nMessage={2}\n",
                  type.FullName, method.ToString(), obsoleteAttrObj.Message);
 
            // 显示结果
            foreach (var result in query) Console.WriteLine(result);
        }

    在一个查询中,可以从执行并行操作换回执行顺序操作,这是通过调用ParallelEnumerable的AsSequential方法做到的:

public static IEnumerable <TSource> AsSequential<TSource>(this ParallelQuery<TSource> source)
    这个方法将一个ParallQuery<T>转换回一个IEnumerable<T>。这样一来,在调用了AsDequential之后执行的操作只由一个线程执行。
    通常,一个LINQ查询的结果数是让某个线程执行一个foreach语句计算获得的。这意味着只有一个线程遍历查询的所有结果。如果希望以并行的方式处理查询的结果,就应该使用ParallelEnumerable的ForAll方法处理查询:
static void ForAll<TSource>(this ParallelQuery<TSource> source,Action<TSource> action)

    这个方法允许多个线程同时 处理结果,可以修改前面的代码来使用该方法:

//显示结果
query.ForAll(Console.WriteLine);

    然而,让多个线程同时调用Console.WriteLine反而会损害性能,因为Console类内部会对线程进行同步,确保每次只有一个线程能访问控制台程序窗口,避免来自多个线程的文本最后显示成一团乱麻。希望为每个结果都执行计算时,才使用ForAll方法。

    由于PLINQ可用多个线程处理数据项,所以数据项被并发处理,结果被无序返回。如果需要让PLINQ保存数据项的顺序,可调用ParallelEnumerable的AsOrderd方法。调用这个方法时,线程会成组处理数据项。然后,这些数据项被合并回去,以保持顺序。这样会损害到性能。一下操作符生成不排序的操作:Distinct,Except,Intersect,Union,Join,GroupBy,GroupJoin和ToLookup。在这些操作符之后,如果想再次强制排序,只需调用AsOrdered方法。
    以下操作符生成排序的操作:ORderBy,OrderByDescending,Thenby和ThenByDescending。在这些出操作符之后,如果想再次恢复不排序的处理,只需调用AsUnordered方法。
    PLINQ提供了一些额外的ParallelEnumerable方法,可调用它们来控制查询的处理方式:
// 设置要与查询关联的 CancellationToken
public static ParallelQuery<TSource> WithCancellation<TSource>(this ParallelQuery<TSource> source, CancellationToken cancellationToken);
// 设置要在查询中使用的并行度。 并行度是将用于处理查询的同时执行的任务的最大数目。
public static ParallelQuery<TSource> WithDegreeOfParallelism<TSource>(this ParallelQuery<TSource> source, int degreeOfParallelism);
// 设置查询的执行模式。
public static ParallelQuery<TSource> WithExecutionMode<TSource>(this ParallelQuery<TSource> source, ParallelExecutionMode executionMode);
//设置此查询的合并选项,它指定查询对输出进行缓冲处理的方式。
public static ParallelQuery<TSource> WithMergeOptions<TSource>(this ParallelQuery<TSource> source, ParallelMergeOptions mergeOptions);
     显然,WithCancellation方法允许传递一个CancellationToken,使查询处理能提前停止。WithDegreeOfParallelism方法指定最多允许多少个线程处理查询;他不会强迫创建满全部线程,如果并不是全部都需要的话。你一般不会调用这个方法。另外,默认情况下,会为每个内核用一个线程来执行查询。
    PLINQ分析一个查询,然后决定如何最好地处理它。有的时候,顺序处理一个查询可以获得更好的性能,尤其在使用以下任何操作时:Concat,ElementAt(OrDefault),First(OrDefault),Last(OrDefault),Skip(While),Task(While)或Zip。使用Select(Many)或Where的重载版本,并向你的selector或predicate委托传递一个位置索引时也是如此。然而,可以调用WithExecutionMode,向它传递某个ParallelExecuteMode标志,从而强迫查询以并行方式处理:
public enum ParallelExecutionMode {
    Default = 0,                // 让并行LINQ决定处理查询的最佳方式
    ForceParallelism = 1   // 强迫查询以其并行方式处理
}

    如前所述,并行LINQ让多个线程处理数据项,结果必须再合并回去。可调用WithMergeOptions向它传递以下某个ParallelMargeOptions标志,从而控制这些结果的缓冲和合并方式: 

    // 指定查询中要使用的输出合并的首选类型。 换言之,它指示 PLINQ 如何将多个分区的结果合并回单个结果序列。 这仅是一个提示,系统在并行处理所有查询时可能不会考虑这一点。
    public enum ParallelMergeOptions
    {
        //   使用默认合并类型,即 AutoBuffered。
        Default = 0,
 
        //  不利用输出缓冲区进行合并。 一旦计算出结果元素,就向查询使用者提供这些元素。
        NotBuffered = 1,
 
        //  利用系统选定大小的输出缓冲区进行合并。 在向查询使用者提供结果之前,会先将结果累计到输出缓冲区中。
        AutoBuffered = 2,
 
        //   利用整个输出缓冲区进行合并。 在向查询使用者提供任何结果之前,系统会先累计所有结果。
        FullyBuffered = 3,
    }

    这些选项使你能在某种程度上控制速度和内存消耗的对应关系。NotBuffered 最省内存,但处理速度慢一些。FullyBuffered 消耗较多内存,但运行得最快。NotBuffered 介于NotBuffered 和FullyBuffered 之间,最好亲自试验所有选项,并对比其性能,来选择那种方式。

 

  八、执行定时计算限制操作

 
    System.Threading命名空间定义了一个Timer类,可用它让一个线程池线程定时调用一个方法。构造Timer类的一个实例相当于告诉线程池:在将来的某个时间会滴哦啊你的一个方法。Timer类提供了几个构造函数,相互都非常相似:
public sealed class Timer : MarshalByRefObject, IDisposable
{
        public Timer(TimerCallback callback, object state, int dueTime, int period);
        public Timer(TimerCallback callback, object state, long dueTime, long period);
        public Timer(TimerCallback callback, object state, TimeSpan dueTime, TimeSpan period);
        public Timer(TimerCallback callback, object state, uint dueTime, uint period);
}

    4个构造器以完全一样的方式构造Timer对象。callback参数标识希望由一个线程池线程回调的方法。当然,你写的对调方法必须和System.Threading.TimerCallback委托类型匹配,如下所示:

delegate void TimerCallback(Object state);

    构造器的state参数允许在每次调用回调方法时都像它传递状态数据;如果没有需要传递的状态数据,可以传递null。dueTime参数告诉CLR在首次调用回调方法之前要等待多少毫秒。可以使用一个有符号或无符号的32位值,一个有符号的64位值或者一个TimeSpan值指定毫秒数。如果希望回调方法立即调用,为dueTime参数指定0即可。最后一个参数(period)指定了以后每次调用回调方法需要等待的时间(毫秒)。如果为这个参数传递Timeout.Infinite(-1),线程池线程值调用回调方法一次。

    在内部,线程池为所有Timer对象只使用了一个线程。这个线程知道下一个Timer对象在什么时候到期。下一个Timer对象到期时,线程就会唤醒,在内部调用TreadPool的QueueUserWorkItem,将一个工作项添加到线程池队列中,使你的回调方法得到调用。如果回调方法的执行时间很长,计时器可能(在上个回调还没有完成时)再次触发。这个能造成多个线程同时执行你的对调方法。为解决这个问题,我的建议是:构造Timer时,为period参数指定Timeout.Infinite。这样,计时器就只触发一次。然后,在你的对调方法中,调用Change方法指定一个新的dueTime,并再次为period参数指定Timeout.Infinite。以下是Change方法的各个重载版本:
public sealed class Timer : MarshalByRefObject, IDisposable
{
      
        public bool Change(int dueTime, int period);
     
        public bool Change(long dueTime, long period);
      
        public bool Change(TimeSpan dueTime, TimeSpan period);
     
        public bool Change(uint dueTime, uint period);
}

    Timer类还提供了Dispose方法,运行完全取消计时器,并可再当时处于pending状态的所有回调完成之后,向notifyObject参数标识的内核对象发送信号。以下是Dispose方法的各个重载版本:

public sealed class Timer : MarshalByRefObject, IDisposable
{
       public void Dispose();
       public bool Dispose(WaitHandle notifyObject);
}

    提示:一个Timer对象被垃圾回收时,它的终结代码告诉线程池取消计时器,使它不再触发。所以,使用一个Timer对象时,要确定有一个变量在保持Timer对象的存货,否则对你的回调方法调用就会停止。

    以下代码演示了如何让一个线程池线程立即开始调用一个回调方法,以后每2秒钟调用一次:
internal static class TimerDemo
        {
            private static Timer s_timer;
 
            public static void Go()
            {
                Console.WriteLine("Main thread: starting a timer");
                using (s_timer = new Timer(ComputeBoundOp, 5, 0, Timeout.Infinite))
                {
                    Console.WriteLine("Main thread: Doing other work here...");
                    Thread.Sleep(10000);
                } // 现在调用Dispose取消计时器
            }
 
            // 一个方法的签名必须符合 TimerCallback 委托
            private static void ComputeBoundOp(Object state)
            {
                // 这个方法由一个线程池线程执行
                Console.WriteLine("In ComputeBoundOp: state={0}", state);
                Thread.Sleep(1000);
 
                // 让 Timer 在2秒钟之后再调用这个方法
                s_timer.Change(2000, Timeout.Infinite);
 
                // 这个方法返回时,线程回归池中,等待下一个工作项
            }
        }

    FCL事实上提供了几个计时器,大多是开发人员都不清楚每个计时器到底有什么独到之处,在这里试着解释一下:

    System.Threading的Timer类    这是刚刚讨论过的计时器。要在一个线程池线程上执行定时的(周期性发生的)后台任务,它是最好的计时器。
    System.Windows.Forms的Timer类    构造这个类的一个实例,相当于告诉Windows将一个计时器和调用线程关联。当这个计时器触发时,WIndows将一条计时器消息(WM_TIMER)注入线程的消息队列。线程必须执行一个消息泵来提取这些消息,并把它们派遣给想要的回调方法。注意,所有这些工作都只有一个线程完成——设置计时器的线程保证就是执行回调方法的线程。这还意味着你的计时器方法不会由多个线程并发执行。
    System.Windows.Threading的DispatcherTimer类    这个类是 System.Windows.Forms的Timer类在Siverlight和WPF应用程序中的等价物。
    System.Timers的Timer类    这个计时器基本是System.Threading的Timer类的一个包装类。当计时器到触发时,会导致CLR将事件放到线程池的队列中。尽量不要使用这个类而是使用System.Threading的Timer类。
 
  九、线程池如何管理线程
  
     1、设置线程池限制
    CLR允许开发人员设置线程池要创建最大线程数。但实践证明,线程池永远都不该为池中的线程数设置上限,因为可能发生饥饿或死锁。假如队列中有1000个工作项,但这些工作项全都因为一个事件而阻塞,等1001个工作项发出信号才能解除阻塞。如果设置最大1000个线程,第1001个工作项就会执行,所有1000个线程都会一直阻塞,最终用户被迫终止应用程序,并丢失他们都做的为保存的工作。
    由于存在饥饿和死锁问题,CLR团队一直都在稳步地增加线程默认能够拥有的最大线程数。目前默认值是最大1000个线程。这基本可以看成是不限数量的,因为一个32位进程最大有2G的可用选址空间。加载一组Win32和CLR DLLs,并分配了本地堆和托管堆之后,剩余约1.5G的地址控制。由于每个线程都要为其用户模式栈和线程环境块(TEB)准备超过1MB的内存,所以一个32位进程中,最多能够有1360个线程。试图创建更多的线程,会抛出一个OutOfMemotyExcption。当然,64位就另当别论了。
    System.Threading.ThreadPool类提供了几个静态方法,可调用它们设置和查询线程池的线程数:GetMaxThreads,SetMaxThreads,GetMinThreads,SetMinThreads和GetAvailableThreads[获得可用的线程数量]。强烈建议你不要调用上述任何方法。限制线程池的线程数,一般只会造成应用程序性能变得更差。
 
    2、如何管理工作者线程
    图 26-1 展示了构成线程池的一部分的工作者线程的各种数据结构。ThreadPool.QueueUserWorkItem方法和Timer类总是将工作项放到全局队列中。工作者线程采用一个先入先出算法将工作项从这个队列中取出来,并处理它们。由于多个工作者线程可能同时从全局队列中拿走工作项,所以所有工作者线程都竞争一个线程同步锁,以保证两个或多个线程不会获取同一个工作项。这个线程同步锁在某些应用程序中可能成为瓶颈,对伸缩性和性能造成某种程序的限制。
 
    现在,让我们谈谈使用默认TaskScheduler(通过查询TaskScheduler的静态Default属性来获得)来调度的Task对象。当一个非工作者线程调度一个Task时,Task会添加到全局队列中。但是,每个工作者线程都有它自己的本地队列。当一个工作者线程调度一个Task时,Task会添加到调用线程的本地队列中。
    一个工作者线程准备好处理一个工作项时,它总是先检查它的本地队列来查找一个Task。入股存在一个Task,工作者线程就从它的本地队列中移出task,并对工作项进行处理。要注意的是,工作者线程采用后入先出的算法将任务从它的本地队列中取出。由于工作者线程是唯一允许访问它自己的本地队列列头的线程,所以无需同步锁,而且在队列中添加和删除Task的速度非常快。这个行为的副作用在于,Task是按照和进入队列时相当的顺序执行的。
    提示:线程池从来不保证排队中的工作项的处理顺序,这是合理的,尤其考虑到多线程可能同事处理工作项。然而,上述副作用使得这个问题变得更加恶化。你必须保证自己的应用程序对工作项或Task的执行顺序不做任何预设。
    如果一个工作者线程发现它的本地队列变空了,工作者线程就会尝试从另一个工作者线程的本例队列中"偷"一个Task。这个Task是从一个本地队列的尾部"偷"走的,并要求获得一个线程同步锁,这对性能可能有少许影响。当然,这种"偷窃"行为很少发生,所以很少需要获取这个锁。如果所有本地队列都变空,那么工作者线程会使用FIFO算法,从全局队列中提取一个工作项。如果全局队列也为空,那么线程就会进入睡眠状态,等待事情的发生。如果睡眠的时间太长,它会自己醒来,并销毁自己,允许系统回收线程使用的资源(包括内核对象、栈、TEB等)。
    线程池会快速常见工作者线程,是工作者线程的数量等于传给ThreadPool的SetMinThreads方法的值。如果从不调用这个方法(也不建议你调用),那么默认值等于你的进程允许使用的CPU数,这是由线程的affinity mask(关联掩码)决定的。通常,你的进程允许使用机器上的所有CPU数,所以线程池创建的工作者线程数量很快就会打到机器上的CPU数。创建了这么多的的线程后,线程池会监视工作项的完成速度。如果工作项完成的时间太长,线程池会创建更多的工作者线程。如果工作项的完成速度开始变快,工作者线程会被销毁。
 
  十、缓存线和伪共享
 
    为了提升反复访问内存的性能,如今的CPU在芯片上都集成了高速缓存。线程首次从RAM去取一些值时,CPU从RAM获取所需的值,并把它存储到CPU的高速缓存中。事实上,为了进一步提升性能,CPU会在逻辑上将所有内存都划分为所谓的缓冲行(cache line)。一个缓冲行有64个字节构成,所以CPU从RAM中获取并存储64字节的块。如果应用程序需要读取一个Int32值,那么会获取包含了那个Intt32的64个字节,这样会获取到比需要的更多字节,这样通常会造成性能增强,因为大多数应用程序在访问了一些数据之后,通常会继续访问存储在那些数据周围的数据。由于相邻的数据已经提取到CPU的缓存中,就避免了慢速的RAM访问。
    然而,如果两个或多个内核访问同一个缓冲行中的字节,内核必须相互通信,并在内核之间传递缓冲行,造成多个内核不能同时处理相邻的字节,这对性能会造成严重影响。
    下面用一些代码演示这一点:
internal static class FalseSharing
        {
            private class Data
            {
                // 这两个字段是相邻的,并(极有可能)在相同的缓冲行中
                public Int32 field1;
                public Int32 field2;
            }
 
            private const Int32 iterations = 100000000;
            private static Int32 s_operations = 2;
            private static Int64 s_startTime;
 
            public static void Go()
            {
                // 分配一个对象,并记录开始时间
                Data data = new Data();
                s_startTime = Stopwatch.GetTimestamp();
 
                // 让零个线程访问在对象中它们自己的字段
                ThreadPool.QueueUserWorkItem(o => AccessData(data, 0));
                ThreadPool.QueueUserWorkItem(o => AccessData(data, 1));
 
                //处于测试目的,阻塞Go线程
                Console.ReadLine();
            }
 
            private static void AccessData(Data data, Int32 field)
            {
                // 这里的线程各自访问它们在Data对象中自己的字段
                for (Int32 x = 0; x < iterations; x++)
                {
                    if (field == 0)
                    {
                        data.field1++;
                    }
                    else
                    {
                        data.field2++;
                    }
                }
 
 
                // 不管哪个线程最后结束,都显示它花的时间
                if (Interlocked.Decrement(ref s_operations) == 0)
                {
                    Console.WriteLine("Access time: {0:N0}", Stopwatch.GetTimestamp() - s_startTime);
                }
 
            }
        }

 

    上述代码中,Data对象在构造时包含了两个字段。这两个字段极有可能在同一个缓冲行中。然后,两个线程池线程启动并执行AccessData方法。一个将1加到Data的filed1上的100 000 000 次,另一个线程对filed2字段做同样的事情。每个线程完成后,都递减s_operations字段中的值;最后一个将字段递减为0的线程就是最后一个结束的线程,它显示两个线程完成它们的工作总共发了多少时间。我的机器大约花了245毫秒。
    现在,让我们修改Data类,使它看起来项下面这样:
 
[StructLayout(LayoutKind.Explicit)]
    private class Data {
        // 这两个字段分开了,不再相同的缓冲行中
        [FieldOffset(0)]
        public Int32 field1;
        [FieldOffset(64)]
        public Int32 field2;
    }

    在上述代码中,现在用一个缓存线(64字节)分隔两个字段。再次运行,结果变成了201毫秒,比第一个版本快了一些。从程序角度看,两个线程处理的是不同的数据。但从CPU缓存线来看,CPU处理的是相同的数据。这称为伪共享(false sharing)。在第二个版本中,字段在不同的缓存线上,所以CPU可以真正做到独立,不必共享什么。

    通过上述讨论,应该知道在多个线程同时访问相邻的数据时,缓存线和伪共享可能对应用程序产生严重影响。在性能非常紧要的情形下,这是你应该注意的一点。如果检查到这个问题,通常都可以设计出一种方式来避免它(这里用的就是FiledOffset attribute)。
    要注意的是,数组在数组内存起始处维护着它的长度,具体位置是在前几个数据元素之后,访问一个数组元素时,CLR验证你使用的索引在数组的长度之内。这意味着访问一个数组总是牵涉到访问数组的长度。因此,为了避免产生额外的伪共享,应该避免让一个线程向数组的前几个元素写入,同时让其他线程访问数组中的其他元素。
    
目录
相关文章
|
3月前
|
编译器 数据处理 C#
C#中的异步流:使用IAsyncEnumerable<T>和await foreach实现异步数据迭代
【1月更文挑战第10天】本文介绍了C#中异步流的概念,并通过使用IAsyncEnumerable<T>接口和await foreach语句,详细阐述了如何异步地迭代数据流。异步流为处理大量数据或需要流式处理数据的场景提供了一种高效且非阻塞性的方法,使得开发者能够更优雅地处理并发和数据流问题。
|
1月前
|
算法 C#
23.c#计算敌人被玩家攻击的剩余血值
23.c#计算敌人被玩家攻击的剩余血值
16 0
|
4月前
|
C#
C# 对于“日期时间(DateTime)“的处理 时间差计算
C# 对于“日期时间(DateTime)“的处理 时间差计算
|
4月前
|
算法 数据可视化 C#
C# | Chaikin算法 —— 计算折线对应的平滑曲线坐标点
本文将介绍一种计算折线对应的平滑曲线坐标点的算法。该算法使用Chaikin曲线平滑处理的方法,通过控制张力因子和迭代次数来调整曲线的平滑程度和精度。通过对原始点集合进行切割和插值操作,得到平滑的曲线坐标点集合。实验结果表明,该算法能够有效地平滑折线,并且具有较高的精度和可控性。
53 0
C# | Chaikin算法 —— 计算折线对应的平滑曲线坐标点
|
5月前
|
C#
C#异步详解
c#异步编程原理,await asnyc的使用方法
33 0
|
8月前
|
C# 开发者
C# 开发者技术:进程间数据共享之管道(Pipes)-异步通信版
主要类 1.NamedPipeClientStream 2.NamedPipeServerStream 解释:命名管道是一种进程间通信的方式,它允许不同进程之间在同一台机器上进行通信
446 2
C# 开发者技术:进程间数据共享之管道(Pipes)-异步通信版
|
8月前
|
传感器 机器人 数据处理
Baumer工业相机堡盟工业相机如何使用BGAPISDK的相机图像时间戳计算运行时间以及时间差(C#)
Baumer工业相机堡盟工业相机如何使用BGAPISDK的相机图像时间戳计算运行时间以及时间差(C#)
98 0
|
8月前
|
存储 数据采集 编解码
Baumer工业相机堡盟工业相机如何通过图像尺寸计算图像大小从而计算当前相机传输帧率(C#)
Baumer工业相机堡盟工业相机如何通过图像尺寸计算图像大小从而计算当前相机传输帧率(C#)
69 0
|
8月前
|
传感器 开发工具 C#
Baumer工业相机中偏振相机如何使用Baumer堡盟GAPI SDK来进行偏振数据的计算转换输出(C#)
Baumer工业相机中偏振相机如何使用Baumer堡盟GAPI SDK来进行偏振数据的计算转换输出(C#)
66 0
|
8月前
|
传感器 编解码 监控
Baumer工业相机堡盟工业相机如何通过BGAPISDK里的工具函数来计算工业相机的实时帧率(C#)
Baumer工业相机堡盟工业相机如何通过BGAPISDK里的工具函数来计算工业相机的实时帧率(C#)
39 0