• 异步编程:(TAP)基于任务的异步编程模型详解


    异步编程:(TAP)基于任务的异步编程模型详解

    =============C#.Net 篇目录==============

           最近我为大家陆续介绍了“IAsyncResult异步编程模型 (APM)”和“基于事件的异步编程模式(EAP)”两种异步编程模型。在.NET4.0 中Microsoft又为我们引入了新的异步编程模型“基于任务的异步编程模型(TAP)”,并且推荐我们在开发新的多线程应用程序中首选TAP。那现在我先介绍下TAP具有哪些优势:

    1. 目前版本(.NET4.X)的任务调度器(TaskScheduler)依赖于底层的线程池引擎。通过局部队列的任务内联化(task inlining)和工作窃取机制可以为我们提升程序性能。
    2. 轻松实现任务等待、任务取消、延续任务、异常处理(System.AggregateException)、GUI线程操作。
    3. 在任务启动后,可以随时以任务延续的形式注册回调。
    4. 充分利用现有的线程,避免创建不必要的额外线程。
    5. 结合C#5.0引入async和await关键字轻松实现“异步方法”。

     

    示例源码:异步编程:(TAP)基于任务的异步编程模型详解.rar

     

    术语:

    APM              异步编程模型,Asynchronous Programming Model

    EAP               基于事件的异步编程模式,Event-based Asynchronous Pattern

    TAP               基于任务的异步编程模式,Task-based Asynchronous Pattern

     

    理解CLR线程池引擎、理解全局队列、理解线程的局部队列及性能优势

    1. CLR线程池引擎

    CLR线程池引擎维护了一定数量的空闲工作线程以支持工作项的执行,并且能够重用已有的线程以避免创建新的不必要的线程所花费的昂贵的处理过程。并且使用爬山算法(hill-climbing algorithm)检测吞吐量,判断是否能够通过更多的线程来完成更多的工作项。这个算法的判断依据是工作项所需某些类型资源的可用情况,例如:CPU、网络带宽或其他。此外这个算法还会考虑一个饱和点,即达到饱和点的时候,创建更多地线程反而会降低吞吐量。(线程池的详细介绍请看《异步编程:使用线程池管理线程》

    目前版本的TAP的任务调度器(TaskScheduler)基于CLR线程池引擎实现。当任务调度器(TaskScheduler开始分派任务时:

    1)        在主线程或其他并没有分配给某个特定任务的线程的上下文中创建并启动的任务,这些任务将会在全局队列中竞争工作线程。这些任务被称为顶层任务

    2)        然而,如果是在其他任务的上下文中创建的任务(子任务或嵌套任务),这些任务将被分配在线程的局部队列中。

    嵌套任务:

    是在另一个任务的用户委托中创建并启动的任务。

    子任务:

    是使用TaskCreationOptions.AttachedToParent选项创建顶层任务的嵌套任务或延续任务;或使用TaskContinuationOptions.AttachedToParent选项创建的延续任务的嵌套任务或延续任务。(应用程序使用TaskCreationOptions.DenyChildAttach选项创建父任务。此选项指示运行时会取消子任务的AttachedToParent规范)

    如果你不想特定的任务放入线程的局部队列,那么可以指定TaskCreationOptions.PreferFairness或TaskContinuationOptions.PreferFairness枚举参数。(使Task与ThreadPool.QueueUserWorkItem行为相同)

    1. 线程池的全局队列

           当调用ThreadPool.QueueUserWorkItem()添加工作项时,该工作项会被添加到线程池的全局队列中。线程池中的空闲线程以FIFO的顺序将工作项从全局队列中取出并执行,但并不能保证按某个指定的顺序完成。

           线程的全局队列是共享资源,所以内部会实现一个锁机制。当一个任务内部会创建很多子任务时,并且这些子任务完成得非常快,就会造成频繁的进入全局队列和移出全局队列,从而降低应用程序的性能。基于此原因,线程池引擎为每个线程引入了局部队列。

    1. 线程的局部队列为我们带来两个性能优势:任务内联化(task inlining)和工作窃取机制。

    1)        任务内联化(task inlining)----活用顶层任务工作线程

    我们用一个示例来说明:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    static void Main(string[] args)
    {
        Task headTask= new Task(() =>
        {
            DoSomeWork(null);
        });
        headTask.Start();
        Console.Read();
    }
    private static void DoSomeWork(object obj)
    {
        Console.WriteLine("任务headTask运行在线程“{0}”上",
            Thread.CurrentThread.ManagedThreadId);
     
        var taskTop = new Task(() =>
        {
            Thread.Sleep(500);
            Console.WriteLine("任务taskTop运行在线程“{0}”上",
                Thread.CurrentThread.ManagedThreadId);
        });
        var taskCenter = new Task(() =>
        {
            Thread.Sleep(500);
            Console.WriteLine("任务taskCenter运行在线程“{0}”上",
                Thread.CurrentThread.ManagedThreadId);
        });
        var taskBottom = new Task(() =>
        {
            Thread.Sleep(500);
            Console.WriteLine("任务taskBottom运行在线程“{0}”上",
                Thread.CurrentThread.ManagedThreadId);
        });
        taskTop.Start();
        taskCenter.Start();
        taskBottom.Start();
        Task.WaitAll(new Task[] { taskTop, taskCenter, taskBottom });
    }

    结果:

     

    分析:

           这个示例,我们从Main方法主线程中创建了一个headTask顶层任务并开启。在headTask任务中又创建了三个嵌套任务并最后WaitAll() 这三个嵌套任务执行完成。此时出现的情况就是headTask任务的线程被阻塞,而“任务内联化”技术会使用阻塞的headTask的线程去执行局部队列中的任务。因为减少了对额外线程需求,从而提升了程序性能。

           局部队列“通常”以LIFO的顺序抽取任务并执行,而不是像全局队列那样使用FIFO顺序。LIFO顺序通常用有利于数据局部性,能够在牺牲一些公平性的情况下提升性能。

    数据局部性的意思是:运行最后一个到达的任务所需的数据都还在任何一个级别的CPU高速缓存中可用。由于数据在高速缓存中任然是“热的”,因此立即执行最后一个任务可能会获得性能提升。

    2)        工作窃取机制----活用空闲工作线程

    当一个工作线程的局部队列中有很多工作项正在等待时,而存在一些线程却保持空闲,这样会导致CPU资源的浪费。此时任务调度器(TaskScheduler)会让空闲的工作线程进入忙碌线程的局部队列中窃取一个等待的任务,并且执行这个任务。

     

    由于局部队列为我们带来了性能提升,所以,我们应尽可能地使用TAP提供的服务(任务调度器(TaskScheduler)),而不是直接使用ThreadPool的方法。

     

    任务并行Task

    一个任务表示一个异步操作。任务运行的时候需要使用线程,但并不是说任务取代了线程,理解这点很重要。事实上,在《异步编程:.NET4.X 数据并行》中介绍的System.Threading.Tasks.Parallel类构造的并行逻辑内部都会创建Task,而它们的并行和并发执行都是由底层线程支持的。任务和线程之间也没有一对一的限制关系,通用语言运行时(CLR)会创建必要的线程来支持任务执行的需求。

    1. Task简单的实例成员
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    public class Task : IThreadPoolWorkItem, IAsyncResult, IDisposable
    {
        public Task(Action<object> action, object state
              , CancellationToken cancellationToken,TaskCreationOptions creationOptions);
     
        // 获取此 Task 实例的唯一 ID。
        public int Id { get; }
        // 获取用于创建此任务的TaskCreationOptions。
        public TaskCreationOptions CreationOptions { get; }
        // 获取此任务的TaskStatus。
        public TaskStatus Status { get; }
        // 获取此 Task 实例是否由于被取消的原因而已完成执行。
        public bool IsCanceled { get; }
        // 获取 Task 是否由于未经处理异常的原因而完成。
        public bool IsFaulted { get; }
        // 获取导致 Task 提前结束的System.AggregateException。
        public AggregateException Exception { get; }
     
        #region IAsyncResult接口成员
        private bool IAsyncResult.CompletedSynchronously { get;}
        private WaitHandleIAsyncResult.AsyncWaitHandle { get; }
     
        // 获取在创建 Task 时提供的状态对象,如果未提供,则为 null。
        public object AsyncState { get; }
        // 获取此 Task 是否已完成。
        public bool IsCompleted { get; }
        #endregion
     
        // 释放由 Task 类的当前实例占用的所有资源。
        public void Dispose();
        ……
    }

             分析:

    1)        CancellationToken、IsCancel

    对于长时间运行的计算限制操作来说,支持取消是一件很“棒”的事情。.NET 4.0提供了一个标准的取消操作模式。即通过使用CancellationTokenSource创建一个或多个取消标记CancellationToken(cancellationToken可在线程池中线程或 Task 对象之间实现协作取消),然后将此取消标记传递给应接收取消通知的任意数量的线程或Task对象。当调用CancellationToken关联的CancellationTokenSource对象的Cancle()时,每个取消标记(CancellationToken)上的IsCancellationRequested属性将返回true。异步操作中可以通过检查此属性做出任何适当响应。也可调用取消标记的ThrowIfCancellationRequested()方法来抛出OperationCanceledException异常。

           更多关于CancellationToken与CancellationTokenSource的介绍及示例请看《协作式取消》….

           Task任务中实现取消,可以使用以下几种选项之一终止操作:

    1.                                       i.              简单地从委托中返回。在许多情况下,这样已足够;但是,采用这种方式“取消”的任务实例会转换为RanToCompletion状态,而不是 Canceled 状态。
    2.                                     ii.              创建Task时传入CancellationToken标识参数,并调用关联CancellationTokenSource对象的Cancel()方法:

    a)        如果Task还未开始,那么Task实例直接转为Canceled状态。(注意,因为已经Canceled状态了,所以不能再在后面调用Start())

    b)        (见示例:TaskOperations.Test_Cancel();)如果Task已经开始,在Task内部必须抛出OperationCanceledException异常(注意,只能存在OperationCanceledException异常,可优先考虑使用CancellationToken的ThrowIfCancellationRequested()方法),Task实例转为Canceled状态。

    若对抛出OperationCanceledException异常且状态为Canceled的Task进行等待操作(如:Wait/WaitAll),则会在Catch块中捕获到OperationCanceledException异常,但是此异常指示Task成功取消,而不是有错误的情况。因此IsCancel为true;IsFaulted为false且Exception属性为null。

    1.                                   iii.              对于使用TaskContinuationOptions枚举值为NotOn或OnlyOn创建的延续任务A,在其前面的任务结束状态不匹配时,延续任务A将转换为Canceled状态,并且不会运行。

    2)        TaskCreationOptions枚举

    定义任务创建、调度和执行的一些可选行为。

    None

    指定应使用默认行为。

    PreferFairness

     

    较早安排的任务将更可能较早运行,而较晚安排运行的任务将更可能较晚运行。(Prefer:更喜欢 ; Fair:公平的)

    LongRunning

    该任务需要很长时间运行,因此,调度器可以对这个任务使用粗粒度的操作(默认TaskScheduler为任务创建一个专用线程,而不是排队让一个线程池线程来处理,可通过在延续任务中访问:Thread.CurrentThread.IsThreadPoolThread属性判别)。比如:如果任务可能需要好几秒的时间运行,那么就使用这个参数。相反,如果任务只需要不到1秒钟的时间运行,那么就不应该使用这个参数。

    AttachedToParent

    指定此枚举值的Task,其内部创建的Task或通过ContinueWith()创建的延续任务都为子任务。(父级是顶层任务)

    DenyChildAttach

    如果尝试附加子任务到创建的任务,指定System.InvalidOperationException将被引发。

    HideScheduler

    创建任务的执行操作将被视为TaskScheduler.Default默认计划程序。

    3)        IsCompleted

    Task实现了IAsyncResult接口。在任务处于以下三个最终状态之一时IsCompleted返回 true:RanToCompletion、 Faulted 或 Canceled。

    4)        TaskStatus枚举

    表示 Task 的生命周期中的当前阶段。一个Task实例只会完成其生命周期一次,即当Task到达它的三种可能的最终状态之一时,Task就结束并释放。

    可能的初始状态

    Created

    该任务已初始化,但尚未被计划。

    WaitingForActivation

    只有在其它依赖的任务完成之后才会得到调度的任务的初始状态。这种任务是使用定义延续的方法创建的。

    WaitingToRun

    该任务已被计划执行,但尚未开始执行。

    中间状态

    Running

    该任务正在运行,但尚未完成。

    WaitingForChildrenToComplete

    该任务已完成执行,正在隐式等待附加的子任务完成。

    可能的最终状态

    RanToCompletion

    已成功完成执行的任务。

    Canceled

    该任务已通过对其自身的CancellationToken引发OperationCanceledException异常

    Faulted

    由于未处理异常的原因而完成的任务。

           状态图如下:

                     

    5)        Dispose()

    尽管Task为我们实现了IDisposable接口,但依然不推荐你主动调用Dispose()方法,而是由系统终结器进行清理。原因:

    a)        Task调用Dispose()主要释放的资源是WaitHandle对象。

    b)        .NET4.5 对.NET4.0 中提出的Task进行过大量的优化,让其尽量不再依赖WaitHandle对象(eg:.NET4.0种Task的WaitAll()/WaitAny()的实现依赖于WaitHandle)。

    c)        在使用Task时,大多数情况下找不到一个好的释放点,保证该Task已经完成并且没有被其他地方在使用。

    d)        Task.Dispose()方法在“.NET Metro风格应用程序”框架所引用的程序集中甚至并不存在(即此框架中Task没有实现IDisposable接口)。

    更详细更专业的Dispose()讨论请看《.NET4.X并行任务Task需要释放吗?》

    1. Task的实例方法
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
            // 获取用于等待此 Task 的等待者。
            public TaskAwaiter GetAwaiter();
            // 配置用于等待此System.Threading.Tasks.Task的awaiter。
            // 参数:continueOnCapturedContext:
            //     试图在await返回时夺取原始上下文,则为 true;否则为 false。
            public ConfiguredTaskAwaitable ConfigureAwait(bool continueOnCapturedContext);
     
            // 对提供的TaskScheduler同步运行 Task。
            public void RunSynchronously(TaskScheduler scheduler);
            // 启动 Task,并将它安排到指定的TaskScheduler中执行。
            public void Start(TaskScheduler scheduler);
            // 等待 Task 完成执行过程。
            public bool Wait(int millisecondsTimeout, CancellationToken cancellationToken);
     
            // 创建一个在目标 Task 完成时执行的延续任务。
            public Task ContinueWith(Action<Task, object> continuationAction, object state
                    , CancellationToken cancellationToken
                    , TaskContinuationOptions continuationOptions, TaskScheduler scheduler);
    public Task<TResult>ContinueWith<TResult>(
            Func<Task, object, TResult> continuationFunction
                    , object state,CancellationToken cancellationToken
                    , TaskContinuationOptions continuationOptions, TaskScheduler scheduler);
            ……

             分析:

    1)        TaskContinuationOptions

    在创建一个Task作为另一个Task的延续时,你可以指定一个TaskContinuationOptions参数,这个参数可以控制延续另一个任务的任务调度和执行的可选行为。

    None

    默认情况下,完成前面的任务之后“都”将安排运行延续任务,而不考虑前面任务的最终TaskStatus。

    AttachedToParent

    对延续任务指定此枚举值,表示该延续任务内部创建的新Task或通过ContinueWith()创建的延续任务都为子任务。(父级是延续任务)

    PreferFairness

    LongRunning

    DenyChildAttach           

    HideScheduler

     

     

    参考:TaskCreationOptions枚举 

    LazyCancellation

    在延续取消的情况下,防止延续的完成直到完成先前的任务。

    NotOnRanToCompletion

    NotOnFaulted

    NotOnCanceled

    指定不应在延续任务前面的任务“已完成运行、引发了未处理异常、已取消”的情况下安排延续任务。

     

    此选项对多任务延续无效。

    OnlyOnCanceled

    OnlyOnFaulted

    OnlyOnRanToCompletion

    指定只应在延续任务前面的任务“已取消、引发了未处理异常、已完成运行”的情况下才安排延续任务。

    ExecuteSynchronously

    指定应同步执行延续任务。指定此选项后,延续任务将在导致前面的任务转换为其最终状态的相同线程上运行。

    注意:

    a)        如果使用默认选项TaskContinuationOptions.None,并且之前的任务被取消了,那么延续任务任然会被调度并启动执行。

    b)        如果该条件在前面的任务准备调用延续时未得到满足,则延续将直接转换为 Canceled 状态,之后将无法启动。

    c)        如果调用多任务延续(即:调用TaskFactory或TaskFactory<TResult>的静态ContinueWhenAll和ContinueWhenAny方法)时,NotOn和OnlyOn六个标识或标识的组合都是无效的。也就是说,无论先驱任务是如何完成的,ContinueWhenAll和ContinueWhenAny都会执行延续任务。

    d)        TaskContinuationOptions.ExecuteSynchronously,指定同步执行延续任务。延续任务会使用前一个任务的数据,而保持在相同线程上执行就能快速访问高速缓存中的数据,从而提升性能。此外,也可避免调度这个延续任务产生不必要的额外线程开销。

    如果在创建延续任务时已经完成前面的任务,则延续任务将在创建此延续任务的线程上运行。只应同步执行运行时间非常短的延续任务。

    2)        开启任务

    只有Task处于TaskStatus.Created状态时才能使用实例方法Start()。并且,只有在使用Task的公共构造函数构造的Task实例才能处于TaskStatus.Created状态。

    当然我们还知道有其他方式可以创建Task并开启任务,比如Task.Run()/Task.ContinueWith()/Task.Factory.StartNew()/TaskCompletionSource/异步方法(即使用async与await关键字的方法),但是这些方法返回的Task已经处于开启状态,即不能再调用Start()。更丰富更专业的讨论请看《.NET4.X 并行任务中Task.Start()的FAQ》

    3)        延续任务ContinueWith

    a)        ContinueWith() 方法可创建一个根据TaskContinuationOptions参数限制的延续任务。可以为同一个Task定义多个延续任务让它们并行执行。

    比如,为t1定义两个并行延续任务t2、t3.

    1
    2
    3
    Task<int> t1 = new Task<int>(() => { return 1; });
    Task<int> t2 = t1.ContinueWith<int>(Work1,……);
    Task<int> t3 = t1.ContinueWith<int>(Work1,……);

    b)        调用Wait()方法和Result属性会导致线程阻塞,极有可能造成线程池创建一个新线程,这增大了资源的消耗,并损害了伸缩性。可以在延续任务中访问这些成员,并做相应操作。

    c)        对前面任务的引用将以参数形式传递给延续任务的用户委托,以将前面任务的数据传递到延续任务中。

    4)        Wait()

    一个线程调用Wait()方法时,系统会检查线程要等待的Task是否已开始执行。

    a)        如果是,调用Wait()的线程会阻塞,直到Task运行结束为止。

    b)        如果Task还没有开始执行,系统可能(取决于TaskScheduler)使用调用Wait()的线程来执行Task。如果发生这种情况,那么调用Wait()的线程不会阻塞;它会执行Task并立刻返回。

    1.                                        i.              这样做的好处在于,没有线程会被阻塞,所以减少了资源的使用(因为不需要创建一个线程来替代被阻塞的线程),并提升了性能(因为不需要花时间创建一个线程,也没有上下文切换)。
    2.                                      ii.              但不好的地方在于,假如线程在调用Wait()前已经获得一个不可重入的线程同步锁(eg:SpinLock),而Task试图获取同一个锁,就会造成一个死锁的线程!

    5)        RunSynchronously

    可在指定的TaskScheduler或TaskScheduler.Current中同步运行 Task。即RunSynchronously()之后的代码会阻塞到Task委托执行完毕。

    示例如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    Task task1 = new Task(() =>
    {
        Thread.Sleep(5000);
        Console.WriteLine("task1执行完毕。");
    });
    task1.RunSynchronously();
    Console.WriteLine("执行RunSynchronously()之后的代码。");
     
    // 输出==============================
    // task1执行完毕。
    // 执行RunSynchronously()之后的代码。
    1. Task的静态方法
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    // 返回当前正在执行的 Task 的唯一 ID。
    public static int? CurrentId{ get; }
    // 提供对用于创建 Task 和 Task<TResult>实例的工厂方法的访问。
    public static TaskFactory Factory { get; }
    // 创建指定结果的、成功完成的Task<TResult>。
    public static Task<TResult> FromResult<TResult>(TResult result);
     
    // 创建将在指定延迟后完成的任务。
    public static Task Delay(int millisecondsDelay, CancellationToken cancellationToken);
     
    // 将在线程池上运行的指定工作排队,并返回该工作的任务句柄。
    public static Task Run(Action action, CancellationToken cancellationToken);
    // 将在线程池上运行的指定工作排队,并返回该工作的 Task(TResult) 句柄。
    public static Task<TResult>  Run<TResult>(Func<TResult> function, CancellationToken  cancellationToken);
    // 将在线程池上运行的指定工作排队,并返回 function 返回的任务的代理项。
    public static Task Run(Func<Task> function, CancellationToken cancellationToken);
    // 将在线程池上运行的指定工作排队,并返回 function 返回的 Task(TResult) 的代理项。
    public static Task<TResult> Run<TResult>(Func<Task<TResult>> function, CancellationToken cancellationToken);
     
    // 等待提供的所有 Task 对象完成执行过程。
    public static bool WaitAll(Task[] tasks, intmillisecondsTimeout, CancellationToken cancellationToken);
    // 等待提供的任何一个 Task 对象完成执行过程。
    // 返回结果:
    //     已完成的任务在 tasks 数组参数中的索引,如果发生超时,则为 -1。
    public static int WaitAny(Task[] tasks, int millisecondsTimeout, CancellationToken cancellationToken);
     
    // 所有提供的任务已完成时,创建将完成的任务。
    public static Task WhenAll(IEnumerable<Task> tasks);
    public static Task<TResult[]> WhenAll<TResult>(IEnumerable<Task<TResult>> tasks);
    // 任何一个提供的任务已完成时,创建将完成的任务。
    public static Task<Task> WhenAny(IEnumerable<Task> tasks);
    public static Task<Task<TResult>> WhenAny<TResult>(IEnumerable<Task<TResult>> tasks);
     
    // 创建awaitable,等待时,它异步产生当前上下文。
    // 返回结果:等待时,上下文将异步转换回等待时的当前上下文。
    // 如果当前SynchronizationContext不为 null,则将其视为当前上下文。
    // 否则,与当前执行任务关联的任务计划程序将视为当前上下文。
    public static YieldAwaitable Yield();

             分析:

    1)        FromResult<TResult>(TResult result);

    创建指定结果的、成功完成的Task<TResult>。我们可以使用此方法创建包含预先计算结果/缓存结果的 Task<TResult>对象,示例代码或CachedDownloads.cs示例文件。

    2)        Delay

    创建将在指定延迟后完成的任务,返回Task。可以通过await或Task.Wait()来达到Thread.Sleep()的效果。尽管,Task.Delay() 比Thread.Sleep()消耗更多的资源,但是Task.Delay()可用于为方法返回Task类型;或者根据CancellationToken取消标记动态取消等待。

    Task.Delay()等待完成返回的Task状态为RanToCompletion;若被取消,返回的Task状态为Canceled。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    var tokenSource = new CancellationTokenSource();
    var token = tokenSource.Token;
    Task.Factory.StartNew(() => { Thread.Sleep(1000); tokenSource.Cancel(); });
    Console.WriteLine("Begin taskDelay1");
    Task taskDelay1 = Task.Delay(100000, token);
    try
    {
        taskDelay1.Wait();
    }
    catch (AggregateException ae)
    {
        foreach (var v in ae.InnerExceptions)
            Console.WriteLine(ae.Message + " " + v.Message);
    }
    taskDelay1.ContinueWith((t) =>Console.WriteLine(t.Status.ToString()));
     
    Thread.Sleep(100);
    Console.WriteLine();
     
    Console.WriteLine("Begin taskDelay2");
    Task taskDelay2 = Task.Delay(1000);
    taskDelay2.ContinueWith((t) =>Console.WriteLine(t.Status.ToString()));
    // 输出======================================
    // Begin taskDelay1
    // 发生一个或多个错误。已取消一个任务。
    // Canceled
    //
    // Begin taskDelay2
    // Completed
    1. Task<TResult>:Task

    Task<TResult>继承自Task,表示一个可以返回值的异步操作,提供Result只读属性用于访问异步操作的返回值。该属性会阻塞线程,直到Task执行完毕并返回值。

     

    System.Threading.Tasks.TaskFactory         

    1. 设置共用\默认的参数

    通过TaskFactory对象提供的Scheduler、CancellationToken、CreationOption和ContinuationOptions属性可以为Task设置共用\默认的参数,以便快捷的创建Task或延续任务。影响StartNew()、ContinueWhenAll()|ContinueWhenAny()、FromAsync()方法的默认参数设置。

    1. StartNew()

    Task.Factory.StartNew()可快速创建一个Task并且开启任务。代码如下:

    1
    var t = Task.Factory.StartNew(someDelegate);

    这等效于:

    1
    2
    var t = new Task(someDelegate);
    t.Start();

    表现方面,前者更高效。Start()采用同步方式运行以确保任务对象保持一致的状态即使是同时调用多次Start(),也可能只有一个调用会成功。相比之下,StartNew()知道没有其他代码能同时启动任务,因为在StartNew()返回之前它不会将创建的Task引用给任何人,所以StartNew()不需要采用同步方式执行。更丰富更专业的讨论请看《.NET4.X 并行任务中Task.Start()的FAQ》

    1. ContinueWhenAll()
    1
    2
    3
    public Task ContinueWhenAll(Task[] tasks, Action<Task[]> continuationAction
        , CancellationToken cancellationToken
        , TaskContinuationOptions continuationOptions, TaskScheduler scheduler);

    创建一个延续 Task 或延续 Task<TResult>,它将在提供的一组任务完成后马上开始。延续任务操作委托接受一个Task[]数组做参数。

    1. ContinueWhenAny()
    1
    2
    3
    public Task ContinueWhenAny(Task[] tasks, Action<Task> continuationAction
        , CancellationToken cancellationToken
        , TaskContinuationOptions continuationOptions, TaskScheduler scheduler);

    创建一个延续 Task 或延续 Task<TResult>,它将在提供的组中的任何一个任务完成后马上开始。延续任务操作委托接受一个 Task 做参数。

    1. 通过Task.TaskFactory.FromAsync() 实例方法,我们可以将APM转化为TAP。示例见此文的后面小节AMP转化为TAP和EAP转化为TAP

     

    System.Threading.Tasks.TaskScheduler        

           TaskScheduler表示一个处理将任务排队到线程中的底层工作对象。TaskScheduler通常有哪些应用呢?

    1. TaskScheduler是抽象类,可以继承它实现自己的任务调度计划。如:默认调度程序ThreadPoolTaskScheduler、与SynchronizationContext.Current关联的SynchronizationContextTaskScheduler。
    2. 由TaskScheduler.Default获取默认调度程序ThreadPoolTaskScheduler。
    3. 由TaskScheduler.Current获取当前任务的执行的TaskScheduler。
    4. 由 TaskScheduler.TaskSchedulerFromCurrentSynchronizationContext() 方法获取与SynchronizationContext.Current关联的SynchronizationContextTaskScheduler,SynchronizationContextTaskScheduler上的任务都会通过SynchronizationContext.Post()在同步上下文中进行调度。通常用于实现跨线程更新控件。
    5. 通过MaximumConcurrencyLevel设置任务调度计划能支持的最大并发级别。
    6. 通过UnobservedTaskException事件捕获未被观察到的异常。

     

    System.Threading.Tasks.TaskExtensions

    提供一组用于处理特定类型的 Task 实例的静态方法。将特定Task实例进行解包操作。

    1
    2
    3
    4
    5
    public static class TaskExtensions
    {
        public static Task<TResult> Unwrap<TResult>(this Task<Task<TResult>> task);
        public static Task Unwrap(this Task<Task> task);
    }

     

    AMP转化为TAP和EAP转化为TAP

    1. AMP转化为TAP

    通过Task.TaskFactory.FromAsync() 实例方法,我们可以将APM转化为TAP。

    注意点:

    1)        FromAsync方法返回的任务具有WaitingForActivation状态,并将在创建该任务后的某一时间由系统启动。如果尝试在这样的任务上调用 Start,将引发异常。

    2)        转化的APM异步模型必须符合两个模式:

    a)        接受Begin***和End***方法。此时要求Begin***方法签名的委托必须是AsyncCallback以及 End***方法只接受IAsyncResult一个参数。此模式AsyncCallback回调由系统自动生成,主要工作是调用End***方法。

    1
    2
    3
    4
    public Task<TResult> FromAsync<TArg1, TResult>(
          Func<TArg1, AsyncCallback, object, IAsyncResult> beginMethod
        , Func<IAsyncResult, TResult> endMethod, TArg1 arg1
        , object state, TaskCreationOptions creationOptions);

    b)        接受IAsyncResult对象以及End***方法。此时Begin***方法的签名已经无关紧要只要能返回IAsyncResult的参数以及 End***方法只接受IAsyncResult一个参数。此模式支持自定义回调委托。

    1
    2
    public Task<TResult> FromAsync<TResult>(IAsyncResult asyncResult
        , Func<IAsyncResult, TResult> endMethod);

    3)        当然,我们有时需要给客户提供统一的 Begin***() 和 End***() 调用方式,我们可以直接使用Task从零开始构造APM。即:在 Begin***() 创建并开启任务,并返回Task。因为Task是继承自IAsyncResult接口的,所以我们可以将其传递给 End***() 方法,并在此方法里面调用Result属性来等待任务完成。

    4)        对于返回的Task,可以随时以任务延续的形式注册回调。

    现在将在《APM异步编程模型》博文中展现的示例转化为TAP模式。关键代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    public Task<int> CalculateAsync<TArg1, TArg2>(
          Func<TArg1, TArg2, AsyncCallback, object, IAsyncResult> beginMethod
        , AsyncCallback userCallback, TArg1 num1, TArg2 num2, object asyncState)
    {
        IAsyncResult result = beginMethod(num1, num2, userCallback, asyncState);
        return Task.Factory.FromAsync<int>(result
                , EndCalculate, TaskCreationOptions.None);
    }
     
    public Task<int> CalculateAsync(int num1, int num2, object asyncState)
    {
        return Task.Factory.FromAsync<int, int, int>(BeginCalculate, EndCalculate
                , num1, num2, asyncState, TaskCreationOptions.None);
    }
    1. EAP转化为TAP

    我们可以使用TaskCompletionSource<TResult>实例将EAP操作表示为一个Task<TResult>。

    TaskCompletionSource<TResult>表示未绑定委托的Task<TResult>的制造者方,并通过TaskCompletionSource<TResult>.Task属性获取由此Tasks.TaskCompletionSource<TResult>创建的Task<TResult>。

    注意,TaskCompletionSource<TResult>创建的任何任务将由TaskCompletionSource启动,因此,用户代码不应在该任务上调用 Start()方法。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    public class TaskCompletionSource<TResult>
    {
        public TaskCompletionSource();
        // 使用指定的状态和选项创建一个TaskCompletionSource<TResult>。
        //   state: 要用作基础 Task<TResult>的AsyncState的状态。
        public TaskCompletionSource(object state, TaskCreationOptions creationOptions);
     
        // 获取由此Tasks.TaskCompletionSource<TResult>创建的Tasks.Task<TResult>。
        public Task<TResult> Task { get; }
     
        // 将基础Tasks.Task<TResult>转换为Tasks.TaskStatus.Canceled状态。
        public void SetCanceled();
        public bool TrySetCanceled();
     
        // 将基础Tasks.Task<TResult>转换为Tasks.TaskStatus.Faulted状态。
        public void SetException(Exception exception);
        public void SetException(IEnumerable<Exception> exceptions);
        public bool TrySetException(Exception exception);
        public bool TrySetException(IEnumerable<Exception> exceptions);
     
        // 尝试将基础Tasks.Task<TResult>转换为TaskStatus.RanToCompletion状态。
        public bool TrySetResult(TResult result);
        ……       
    }

    现在我将在《基于事件的异步编程模式(EAP)》博文中展现的BackgroundWorker2组件示例转化为TAP模式。

    我们需要修改地方有:

    1)        创建一个TaskCompletionSource<int>实例tcs;

    2)        为tcs.Task返回的任务创建延续任务,延续任务中根据前面任务的IsCanceled、IsFaulted、Result等成员做逻辑;

    3)        Completed事件,在这里面我们将设置返回任务的状态。

    关键代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
        // 1、创建 TaskCompletionSource<TResult>
    tcs = new TaskCompletionSource<int>();
    worker2.RunWorkerCompleted += RunWorkerCompleted;
        // 2、注册延续
    tcs.Task.ContinueWith(t =>
    {
            if (t.IsCanceled)
                MessageBox.Show("操作已被取消");
            else if (t.IsFaulted)
                MessageBox.Show(t.Exception.GetBaseException().Message);
            else
                MessageBox.Show(String.Format("操作已完成,结果为:{0}", t.Result));
        }, TaskContinuationOptions.ExecuteSynchronously);
        // 3、运行异步任务
        worker2.RunWorkerAsync();
        // 4、Completed事件
        private void RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
        {
            if (e.Error != null)
                tcs.SetException(e.Error);
            else if (e.Cancelled)
                tcs.SetCanceled();
            else
                tcs.SetResult((int)e.Result);
            // 注销事件,避免多次挂接事件
            worker2.RunWorkerCompleted -= RunWorkerCompleted;
        }

     

    当然,这两部分的代码都不能直接运行,只是部分关键代码,完整的示例请在我提供的示例源码中查看。

     

    使用关键字async和await实现异步方法

           在C#5.0中引入了async和await关键字,可以方便我们使用顺序结构流(即不用回调)来实现异步编程,大大降低了异步编程的复杂程度。(vs2010打 Visual Studio Async CTP for VS2010补丁可以引入关键字”async”和”await”的支持,但是得不到.net4.5新增API的支持)

    1. 我们可通过下图来明白异步方法的构建和异步方法的执行流程。(代码详见我提供的示例程序)

           

     

    1. 编译器转换

    使用 async 关键字标记方法,会导致 C# 或 Visual Basic 编译器使用状态机重新编写该方法的实施。借助此状态机,编译器可以在该方法中插入多个中断点,以便该方法可以在不阻止线程的情况下,挂起和恢复其执行。这些中断点不会随意地插入。它们只会在您明确使用 await 关键字的位置插入:

    1
    2
    3
    4
    5
    6
    private async void btnDoWork_Click(object sender, EventArgs e)
    {
        ...
        await someObject; // <-- potential method suspension point
        ...
    }

    当您等待未完成的异步操作时,编译器生成的代码可确保与该方法相关的所有状态(例如,局部变量)封装并保留在堆中。然后,该函数将返回到调用程序,允许在其运行的线程中执行其他任务。当所等待的异步操作在稍后完成时,该方法将使用保留的状态恢复执行。

    任何公开 await 模式的类型都可以进行等待。该模式主要由一个公开的 GetAwaiter()方法组成,该方法会返回一个提供 IsCompleted、OnCompleted 和 GetResult 成员的类型。当您编写以下代码时:

    1
    await someObject;

    编译器会生成一个包含 MoveNext 方法的状态机类:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    private class FooAsyncStateMachine : IAsyncStateMachine
    {
        // Member fields for preserving “locals” and other necessary     state
        int $state;
        TaskAwaiter $awaiter;
        
        public void MoveNext()
        {
            // Jump table to get back to the right statement upon         resumption
            switch (this.$state)
            {
                
            case 2: goto Label2;
                
            }
            
            // Expansion of “await someObject;”
            this.$awaiter = someObject.GetAwaiter();
            if (!this.$awaiter.IsCompleted)
            {
                this.$state = 2;
                this.$awaiter.OnCompleted(MoveNext);
                return;
                Label2:
            }
            this.$awaiter.GetResult();
            
        }
    }

    在实例someObject上使用这些成员来检查该对象是否已完成(通过 IsCompleted),如果未完成,则挂接一个续体(通过 OnCompleted),当所等待实例最终完成时,系统将再次调用 MoveNext 方法,完成后,来自该操作的任何异常将得到传播或作为结果返回(通过 GetResult),并跳转至上次执行中断的位置。

    1. 自定义类型支持等待

    如果希望某种自定义类型支持等待,我们可以选择两种主要的方法。

    1)        一种方法是针对自定义的可等待类型手动实施完整的 await 模式,提供一个返回自定义等待程序类型的 GetAwaiter 方法,该等待程序类型知道如何处理续体和异常传播等等。

    2)        第二种实施该功能的方法是将自定义类型转换为任务,然后只需依靠对等待任务的内置支持来等待特殊类型。前文所展示的“EAP转化为TAP”正属于这一类,关键代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    private async void btn_Start_Click(object sender, EventArgs e)
    {
        this.progressBar1.Value = 0;
     
        tcs = new TaskCompletionSource<int>();
        worker2.RunWorkerCompleted += RunWorkerCompleted;
        tcs.Task.ContinueWith(t =>
        {
            if (t.IsCanceled)
                MessageBox.Show("操作已被取消");
            else if (t.IsFaulted)
                MessageBox.Show(t.Exception.GetBaseException().Message);
            else
                MessageBox.Show(String.Format("操作已完成,结果为:{0}", t.Result));
        }, TaskContinuationOptions.ExecuteSynchronously);
     
        worker2.RunWorkerAsync();
        await tcs.Task;
    }

     

    处理TAP中的异常

           在任务抛出的未处理异常都封装在System.AggregateException对象中。这个对象会存储在方法返回的Task或Task<TResult>对象中,需要通过访问Wait()、Result、Exception成员才能观察到异常。(所以,在访问Result之前,应先观察IsCanceled和IsFaulted属性)

    1. AggregateException对象的三个重要成员

    1)        InnerExceptions属性

    获取导致当前异常的System.Exception实例的只读集合(即,ReadOnlyCollection<Exception>)。不要将其与基类Exception提供的InnerException属性混淆。

    2)        Flatten() 方法

    遍历InnerExceptions异常列表,若列表中包含类型为AggregateException的异常,就移除所有嵌套的AggregateException,直接返回其真真的异常信息(效果如下图)。

                     

    1)        Handle(Func<Exception, bool> predicate)方法

    它为AggregateException中包含的每个异常都调用一个回调方法。然后,回调方法可以为每个异常决定如何对其进行处理,回调返回true表示异常已经处理,返回false表示没有。在调用Handle之后,如果至少有一个异常没有处理,就创建一个新的AggregateException对象,其中只包含未处理的异常,并抛出这个新的AggregateException对象。

    比如:将任何OperationCanceledException对象都视为已处理。其他任何异常都造成抛出一个新的AggregateException,其中只包含未处理的异常。

    1
    2
    3
    4
    5
    try{……}
    catch (AggregateException ae)
    {
        ae.Handle(e => e is OperationCanceledException);
    }
    1. 父任务生成了多个子任务,而多个子任务都抛出了异常

    1)        嵌套子任务

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    Task t4 = Task.Factory.StartNew(() =>
    {
        Task.Factory.StartNew(() => { throw new Exception("子任务Exception_1"); }
                , TaskCreationOptions.AttachedToParent);
     
        Task.Factory.StartNew(() => { throw new Exception("子任务Exception_2"); }
                , TaskCreationOptions.AttachedToParent);
     
        throw new Exception("父任务Exception");
    });

    对于“嵌套子任务”中子任务的异常都会包装在父任务返回的Task或Task<TResult>对象中。如此例子中 t4.Exception.InnerExceptions的Count为3。

           对于子任务返回的异常类型为包装过的AggregateException对象,为了避免循环访问子任务异常对象的InnerExceptions才能获取真真的异常信息,可以使用上面提到的Flatten() 方法移除所有嵌套的AggregateExceprion。

    2)        Continue子任务

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    Task t1 = Task.Factory.StartNew(() =>
    {
        Thread.Sleep(500);   // 确保已注册好延续任务
        throw new Exception("父任务Exception");
    }, TaskCreationOptions.AttachedToParent);
    Task t2 = t1.ContinueWith((t) =>
    {
        throw new Exception("子任务Exception_1");
    });
    Task t3 = t1.ContinueWith((t) =>
    {
        throw new Exception("子任务Exception_2");
    });

           对于“Continue子任务”中的子任务其异常与父任务是分离的,各自包装在自己返回的Task或 Task<TResult>对象中。如此示例 t1、t2、t3 的Exception.InnerExceptions的Count都为1。    

    1. TaskScheduler的UnobservedTaskException事件

    假如你一直不访问Task的Wait()、Result、Exception成员,那么你将永远注意不到这些异常的发生。为了帮助你检测到这些未处理的异常,可以向TaskScheduler对象的UnobservedTaskException事件注册回调函数。每当一个Task被垃圾回收时,如果存在一个没有注意到的异常,CLR的终结器线程会引发这个事件。

    可在事件回调函数中调用UnobservedTaskExceptionEventArgs对象的SetObserved() 方法来指出已经处理好了异常,从而阻止CLR终止线程。然而并不推荐这么做,宁愿终止进程也不要带着已经损坏的状态继续运行。

    示例代码:(要监控此代码必须在GC.Collect();和事件里两个地方进行断点)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    TaskScheduler.UnobservedTaskException += (s, e) =>
    {
        //设置所有未觉察异常被觉察
        e.SetObserved();
    };
    Task.Factory.StartNew(() =>
    {
        throw new Exception();
    });
    //确保任务完成
    Thread.Sleep(100);
    //强制垃圾会受到,在GC回收时才会触发UnobservedTaskException事件
    GC.Collect();
    //等待终结器处理
    GC.WaitForPendingFinalizers();
    1. 返回void的async“异步方法”中的异常

    我们已经知道返回Task或Task<TResult>对象的任务中抛出的异常会随着返回对象一起返回,可通过Exception属性获取。那么对于返回Task或Task<TResult>对象的“异步方法”情况也是一样。

    然而对于返回void的“异步方法”,方法中抛出的异常会直接导致程序奔溃。

    1
    2
    3
    4
    public static async void Test_void_async_Exception()
    {
        throw new Exception();
    }

    另外,我们还要特别注意lambda表达式构成的“异步方法”,如:

    1
    Enumerable.Range(0, 3).ToList().ForEach(async (i) => { throw new Exception(); });

           本博文到此结束,我相信你看累了,其实我也写了很久…很久…,写完此文,我的“异步编程系列”也算有头有尾了(还会继续扩充)。本博文主要介绍了Task的重要API、任务的CLR线程池引擎、TaskFactory对象、TaskScheduler对象、TaskExtensions对象、AMP转化为TAP和EAP转化为TAP、使用关键字async和await实现异步方法以及自定义类型支持等待、处理TAP中的异常。

    感谢你的观看,如果对你有帮助,还请多多推荐……

     

     

    ===================================================================

    抱歉,我知道你已经累了,但我还是有了这么一段:(其实博文也是写着写着就长起来了!!!)

    此小段是博主的求助,能帮的就帮。

           求职:web中高级工程师 (本人已工作3年)

           我打算5月中旬换一家公司,争取能再端午节6月10之前入职新工作,地点广州(其次,深圳)吧,毕竟还有些朋友在广州。希望园友能推荐一些不错的广州互联网公司。

           PS:我个人有关注下广州3g门户网、广州多益互联网、广州39健康网公司,如你是内部员工我很希望能和你交流下。

           如果你能帮助我:请在博文回复给我,或我的QQ:369220123     邮箱:369220123@qq.com

           如果你只是想与我后续方便讨论问题,请加群: 69594961(.NET开源交流),185718116(广深莞·NET技术)

    ===================================================================

     

     

    推荐阅读:

    关于async与await的FAQ      -----详细讲解了await和async的作用和意义,以及什么是可等待对象、等待者……(此文可帮助你解决80%关于await和async关键字的疑惑)

                   深入探究 WinRT 和 await      -----基于WinRT平板win8系统,讲解了异步功能,以及TPL、编译器转换……

     

     

    参考资料:MSDN

                        书籍:《CLR via C#(第三版)》

    书籍:《C# 并行编程高级教程:精通.NET 4 Parallel Extensions》


    作者:滴答的雨 
    出处:http://www.cnblogs.com/heyuquan/ 
    本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。 

    欢迎园友讨论下自己的见解,及推荐更好资料。 
    本文如对读者有帮助,还请多帮 下此文。 
    谢谢!!!  ( 

     
    分类: C#.Net 篇
    标签: 异步编程
  • 相关阅读:
    Java并发编程:synchronized
    对一致性Hash算法,Java代码实现的深入研究
    在Xcode中使用Git进行源码版本控制
    这些 Git 技能够你用一年了
    简明 Git 命令速查表
    Git 分支管理是一门艺术
    2016年Web前端面试题目汇总
    iOS 面试基础题目
    Json数据交互格式介绍和比较
    SpringBoot端口和访问路径
  • 原文地址:https://www.cnblogs.com/Leo_wl/p/3028301.html
Copyright © 2020-2023  润新知