异步编程:.NET 4.5 基于任务的异步编程模型(TAP)
传送门:异步编程系列目录……
最近我为大家陆续介绍了“IAsyncResult异步编程模型 (APM)”和“基于事件的异步编程模式(EAP)”两种异步编程模型。在.NET4.0 中Microsoft又为我们引入了新的异步编程模型“基于任务的异步编程模型(TAP)”,并且推荐我们在开发新的多线程应用程序中首选TAP,在.NET4.5中更是对TPL库进行了大量的优化与改进。那现在我先介绍下TAP具有哪些优势:
1. 目前版本(.NET4.X)的任务调度器(TaskScheduler)依赖于底层的线程池引擎。通过局部队列的任务内联化(task inlining)和工作窃取机制可以为我们提升程序性能。
2. 轻松实现任务等待、任务取消、延续任务、异常处理(System.AggregateException)、GUI线程操作。
3. 在任务启动后,可以随时以任务延续的形式注册回调。
4. 充分利用现有的线程,避免创建不必要的额外线程。
5. 结合C#5.0引入async和await关键字轻松实现“异步方法”。
示例源码:异步编程:.NET 4.5 基于任务的异步编程模型(TAP).rar
术语:
APM 异步编程模型,Asynchronous Programming Model
EAP 基于事件的异步编程模式,Event-based Asynchronous Pattern
TAP 基于任务的异步编程模式,Task-based Asynchronous Pattern
TPL 任务并行库,Task Parallel Library
理解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行为相同)
2. 线程池的全局队列
当调用ThreadPool.QueueUserWorkItem()添加工作项时,该工作项会被添加到线程池的全局队列中。线程池中的空闲线程以FIFO的顺序将工作项从全局队列中取出并执行,但并不能保证按某个指定的顺序完成。
线程的全局队列是共享资源,所以内部会实现一个锁机制。当一个任务内部会创建很多子任务时,并且这些子任务完成得非常快,就会造成频繁的进入全局队列和移出全局队列,从而降低应用程序的性能。基于此原因,线程池引擎为每个线程引入了局部队列。
3. 线程的局部队列为我们带来两个性能优势:任务内联化(task inlining)和工作窃取机制。
1) 任务内联化(task inlining)----活用顶层任务工作线程
我们用一个示例来说明:
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)会让空闲的工作线程进入忙碌线程的局部队列中窃取一个等待的任务,并且执行这个任务。
由于局部队列为我们带来了性能提升,所以,我们应尽可能地使用TPL提供的服务(任务调度器(TaskScheduler)),而不是直接使用ThreadPool的方法。
任务并行Task
一个任务表示一个异步操作。任务运行的时候需要使用线程,但并不是说任务取代了线程,理解这点很重要。事实上,在《异步编程:.NET4.X 数据并行》中介绍的System.Threading.Tasks.Parallel类构造的并行逻辑内部都会创建Task,而它们的并行和并发执行都是由底层线程支持的。任务和线程之间也没有一对一的限制关系,通用语言运行时(CLR)会创建必要的线程来支持任务执行的需求。
1. Task简单的实例成员
public class Task : IThreadPoolWorkItem, IAsyncResult, IDisposable { public Task(Action
分析:
1) CancellationToken、IsCancel
对于长时间运行的计算限制操作来说,支持取消是一件很“棒”的事情。.NET 4.0提供了一个标准的取消操作模式。即通过使用CancellationTokenSource创建一个或多个取消标记CancellationToken(cancellationToken可在线程池中线程或 Task 对象之间实现协作取消),然后将此取消标记传递给应接收取消通知的任意数量的线程或Task对象。当调用CancellationToken关联的CancellationTokenSource对象的Cancle()时,每个取消标记(CancellationToken)上的IsCancellationRequested属性将返回true。异步操作中可以通过检查此属性做出任何适当响应。也可调用取消标记的ThrowIfCancellationRequested()方法来抛出OperationCanceledException异常。
更多关于CancellationToken与CancellationTokenSource的介绍及示例请看《协作式取消》….
在Task任务中实现取消,可以使用以下几种选项之一终止操作:
i. 简单地从委托中返回。在许多情况下,这样已足够;但是,采用这种方式“取消”的任务实例会转换为RanToCompletion状态,而不是 Canceled 状态。
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。
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需要释放吗?》…
2. Task的实例方法
// 获取用于等待此 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(ActioncontinuationAction, object state , CancellationToken cancellationToken , TaskContinuationOptions continuationOptions, TaskScheduler scheduler); public Task ContinueWith ( Func 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
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.
Taskt1 = new Task (() => { return 1; }); Task t2 = t1.ContinueWith (Work1,……); Task t3 = t1.ContinueWith (Work1,……);
b) 调用Wait()方法和Result属性会导致线程阻塞,极有可能造成线程池创建一个新线程,这增大了资源的消耗,并损害了伸缩性。可以在延续任务中访问这些成员,并做相应操作。
c) 对前面任务的引用将以参数形式传递给延续任务的用户委托,以将前面任务的数据传递到延续任务中。
4) Wait()
一个线程调用Wait()方法时,系统会检查线程要等待的Task是否已开始执行。
a) 如果是,调用Wait()的线程会阻塞,直到Task运行结束为止。
b) 如果Task还没有开始执行,系统可能(取决于局部队列的内联机制)使用调用Wait()的线程来执行Task。如果发生这种情况,那么调用Wait()的线程不会阻塞;它会执行Task并立刻返回。
i. 这样做的好处在于,没有线程会被阻塞,所以减少了资源的使用(因为不需要创建一个线程来替代被阻塞的线程),并提升了性能(因为不需要花时间创建一个线程,也没有上下文切换)。
ii. 但不好的地方在于,假如线程在调用Wait()前已经获得一个不可重入的线程同步锁(eg:SpinLock),而Task试图获取同一个锁,就会造成一个死锁的线程!
5) RunSynchronously
可在指定的TaskScheduler或TaskScheduler.Current中同步运行 Task。即RunSynchronously()之后的代码会阻塞到Task委托执行完毕。
示例如下:
Task task1 = new Task(() => { Thread.Sleep(5000); Console.WriteLine("task1执行完毕。"); }); task1.RunSynchronously(); Console.WriteLine("执行RunSynchronously()之后的代码。"); // 输出============================== // task1执行完毕。 // 执行RunSynchronously()之后的代码。
3. Task的静态方法
// 返回当前正在执行的 Task 的唯一 ID。 public static int? CurrentId{ get; } // 提供对用于创建 Task 和 Task实例的工厂方法的访问。 public static TaskFactory Factory { get; } // 创建指定结果的、成功完成的Task 。 public static Task FromResult (TResult result); // 创建将在指定延迟后完成的任务。 public static Task Delay(int millisecondsDelay, CancellationToken cancellationToken); // 将在线程池上运行的指定工作排队,并返回该工作的任务句柄。 public static Task Run(Action action, CancellationToken cancellationToken); // 将在线程池上运行的指定工作排队,并返回该工作的 Task(TResult) 句柄。 public static Task Run (Func function, CancellationToken cancellationToken); // 将在线程池上运行的指定工作排队,并返回 function 返回的任务的代理项。 public static Task Run(Func function, CancellationToken cancellationToken); // 将在线程池上运行的指定工作排队,并返回 function 返回的 Task(TResult) 的代理项。 public static Task Run (Func > 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 tasks); public static Task WhenAll (IEnumerable > tasks); // 任何一个提供的任务已完成时,创建将完成的任务。 public static Task WhenAny(IEnumerable tasks); public static Task > WhenAny (IEnumerable > tasks); // 创建awaitable,等待时,它异步产生当前上下文。 // 返回结果:等待时,上下文将异步转换回等待时的当前上下文。 // 如果当前SynchronizationContext不为 null,则将其视为当前上下文。 // 否则,与当前执行任务关联的任务计划程序将视为当前上下文。 public static YieldAwaitable Yield();
分析:
1) FromResult
创建指定结果的、成功完成的Task
2) Delay
创建将在指定延迟后完成的任务,返回Task。可以通过await或Task.Wait()来达到Thread.Sleep()的效果。尽管,Task.Delay() 比Thread.Sleep()消耗更多的资源,但是Task.Delay()可用于为方法返回Task类型;或者根据CancellationToken取消标记动态取消等待。
Task.Delay()等待完成返回的Task状态为RanToCompletion;若被取消,返回的Task状态为Canceled。
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
4. Task
Task
System.Threading.Tasks.TaskFactory
1. 设置共用\默认的参数
通过TaskFactory对象提供的Scheduler、CancellationToken、CreationOption和ContinuationOptions属性可以为Task设置共用\默认的参数,以便快捷的创建Task或延续任务。影响StartNew()、ContinueWhenAll()|ContinueWhenAny()、FromAsync()方法的默认参数设置。
2. StartNew()
Task.Factory.StartNew()可快速创建一个Task并且开启任务。代码如下:
var t = Task.Factory.StartNew(someDelegate);
这等效于:
var t = new Task(someDelegate); t.Start();
表现方面,前者更高效。Start()采用同步方式运行以确保任务对象保持一致的状态即使是同时调用多次Start(),也可能只有一个调用会成功。相比之下,StartNew()知道没有其他代码能同时启动任务,因为在StartNew()返回之前它不会将创建的Task引用给任何人,所以StartNew()不需要采用同步方式执行。更丰富更专业的讨论请看《.NET4.X 并行任务中Task.Start()的FAQ》…
3. ContinueWhenAll()
public Task ContinueWhenAll(Task[] tasks, ActioncontinuationAction , CancellationToken cancellationToken , TaskContinuationOptions continuationOptions, TaskScheduler scheduler);
创建一个延续 Task 或延续 Task
4. ContinueWhenAny()
public Task ContinueWhenAny(Task[] tasks, ActioncontinuationAction , CancellationToken cancellationToken , TaskContinuationOptions continuationOptions, TaskScheduler scheduler);
创建一个延续 Task 或延续 Task
5. 通过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实例进行解包操作。
public static class TaskExtensions { public static TaskUnwrap (this Task > task); public static Task Unwrap(this 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***方法。
public TaskFromAsync ( Func beginMethod , Func endMethod, TArg1 arg1 , object state, TaskCreationOptions creationOptions);
b) 接受IAsyncResult对象以及End***方法。此时Begin***方法的签名已经无关紧要只要(即:此模式支持传入自定义回调委托)能返回IAsyncResult的参数以及 End***方法只接受IAsyncResult一个参数。
public TaskFromAsync (IAsyncResult asyncResult , Func endMethod);
3) 当然,我们有时需要给客户提供统一的 Begin***() 和 End***() 调用方式,我们可以直接使用Task从零开始构造APM。即:在 Begin***() 创建并开启任务,并返回Task。因为Task是继承自IAsyncResult接口的,所以我们可以将其传递给 End***() 方法,并在此方法里面调用Result属性来等待任务完成。
4) 对于返回的Task,可以随时以任务延续的形式注册回调。
现在将在《APM异步编程模型》博文中展现的示例转化为TAP模式。关键代码如下:
public TaskCalculateAsync ( Func beginMethod , AsyncCallback userCallback, TArg1 num1, TArg2 num2, object asyncState) { IAsyncResult result = beginMethod(num1, num2, userCallback, asyncState); return Task.Factory.FromAsync (result , EndCalculate, TaskCreationOptions.None); } public Task CalculateAsync(int num1, int num2, object asyncState) { return Task.Factory.FromAsync (BeginCalculate, EndCalculate , num1, num2, asyncState, TaskCreationOptions.None); }
2. EAP转化为TAP
我们可以使用TaskCompletionSource
TaskCompletionSource
注意,TaskCompletionSource
public class TaskCompletionSource{ public TaskCompletionSource(); // 使用指定的状态和选项创建一个TaskCompletionSource 。 // state: 要用作基础 Task 的AsyncState的状态。 public TaskCompletionSource(object state, TaskCreationOptions creationOptions); // 获取由此Tasks.TaskCompletionSource 创建的Tasks.Task 。 public Task Task { get; } // 将基础Tasks.Task 转换为Tasks.TaskStatus.Canceled状态。 public void SetCanceled(); public bool TrySetCanceled(); // 将基础Tasks.Task 转换为Tasks.TaskStatus.Faulted状态。 public void SetException(Exception exception); public void SetException(IEnumerable exceptions); public bool TrySetException(Exception exception); public bool TrySetException(IEnumerable exceptions); // 尝试将基础Tasks.Task 转换为TaskStatus.RanToCompletion状态。 public bool TrySetResult(TResult result); …… }
现在我将在《基于事件的异步编程模式(EAP)》博文中展现的BackgroundWorker2组件示例转化为TAP模式。
我们需要修改地方有:
1) 创建一个TaskCompletionSource
2) 为tcs.Task返回的任务创建延续任务,延续任务中根据前面任务的IsCanceled、IsFaulted、Result等成员做逻辑;
3) Completed事件,在这里面我们将设置返回任务的状态。
关键代码如下:
// 1、创建 TaskCompletionSourcetcs = new TaskCompletionSource (); 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的支持)
异步方法的实现原理
异步方法不需要多线程,因为一个异步方法并不是运行在一个独立的线程中的。
异步方法运行在当前同步上下文中,只有激活的时候才占用当前线程的时间。
异步模型采用时间片轮转来实现。
异步方法的参数和返回值
异步方法的参数:
不能使用“ref”参数和“out”参数,但是在异步方法内部可以调用含有这些参数的方法
异步方法的返回类型:
Task
Task:异步方法没有返回值。
void:主要用于事件处理程序(不能被等待,无法捕获异常)。异步事件通常被认为是一系列异步操作的开始。使用void返回类型不需要await,而且调用void异步方法的函数不会捕获方法抛出的异常。(异步事件中使用await,倘若等待的任务由有异常会导致抛出“调用的目标发生了异常”。当然你可以在异步事件中调用另一个有返回值的异步方法)
异步方法的命名规范
异步方法的方法名应该以Async作为后缀
事件处理程序,基类方法和接口方法,可以忽略此命名规范:例如: startButton_Click不应重命名为startButton_ClickAsync
async和await关键字不会导致其他线程的创建,执行异步方法的线程为其调用线程。而异步方法旨在成为非阻塞操作,即当await等待任务运行时,异步方法会将控制权转移给异步方法外部,让其不受阻塞的继续执行,待await等待的任务执行完毕再将控制权转移给await处,继续执行异步方法后续的代码。
1. 我们可通过下图来明白异步方法的构建和异步方法的执行流程。(代码详见我提供的示例程序async_await_method项目)
需要注意的一个问题:被“async”关键字标记的方法的调用都会强制转变为异步方式吗?
不会,当你调用一个标记了”async”关键字的方法,它会在当前线程以同步的方式开始运行。所以,如果你有一个同步方法,它返回void并且你做的所有改变只是将其标记的“async”,这个方法调用依然是同步的。返回值为Task或Task
方法用“async”关键字标记不会影响方法是同步还是异步运行并完成,而是,它使方法可被分割成多个片段,其中一些片段可能异步运行,这样这个方法可能异步完成。这些片段界限就出现在方法内部显示使用“await”关键字的位置处。所以,如果在标记了“async”的方法中没有显示使用“await”,那么该方法只有一个片段,并且将以同步方式运行并完成。
2. 编译器转换
使用 async 关键字标记方法,会导致 C# 或 Visual Basic 编译器使用状态机重新编写该方法的实现。借助此状态机,编译器可以在该方法中插入多个中断点,以便该方法可以在不阻止线程的情况下,挂起和恢复其执行。这些中断点不会随意地插入。它们只会在您明确使用 await 关键字的位置插入:
private async void btnDoWork_Click(object sender, EventArgs e) { ... await someObject; // <-- potential method suspension point ... }
当您等待未完成的异步操作时,编译器生成的代码可确保与该方法相关的所有状态(例如,局部变量)封装并保留在堆中。然后,该函数将返回到调用程序,允许在其运行的线程中执行其他任务。当所等待的异步操作在稍后完成时,该方法将使用保留的状态恢复执行。
任何公开 await 模式的类型都可以进行等待。该模式主要由一个公开的 GetAwaiter()方法组成,该方法会返回一个提供 IsCompleted、OnCompleted 和 GetResult 成员的类型。当您编写以下代码时:
await someObject;
编译器会生成一个包含 MoveNext 方法的状态机类:
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),并跳转至上次执行中断的位置。
3. 自定义类型支持等待
如果希望某种自定义类型支持等待,我们可以选择两种主要的方法。
1) 一种方法是针对自定义的可等待类型手动实现完整的 await 模式,提供一个返回自定义等待程序类型的 GetAwaiter 方法,该等待程序类型知道如何处理续体和异常传播等等。
2) 第二种实施该功能的方法是将自定义类型转换为Task,然后只需依靠对等待任务的内置支持来等待特殊类型。前文所展示的“EAP转化为TAP”正属于这一类,关键代码如下:
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(); // void的异步方法:主要用于事件处理程序(不能被等待,无法捕获异常)。异步事件通常被认为 // 是一系列异步操作的开始。使用void返回类型不需要await,而且调用void异步方法的函数不 // 会捕获方法抛出的异常。(异步事件中使用await,倘若等待的任务由有异常会导致 // 抛出“调用的目标发生了异常”。当然你可以在异步事件中调用另一个有返回值的异步方法) // 所以不需要下面的await,因为会出现在执行取消后拖动界面会因异常被观察到并且终止整个进程 // await tcs.Task; }
处理TAP中的异常
在任务抛出的未处理异常都封装在System.AggregateException对象中。这个对象会存储在方法返回的Task或Task
1. AggregateException对象的三个重要成员
1) InnerExceptions属性
获取导致当前异常的System.Exception实例的只读集合(即,ReadOnlyCollection
2) Flatten() 方法
遍历InnerExceptions异常列表,若列表中包含类型为AggregateException的异常,就移除所有嵌套的AggregateException,直接返回其真真的异常信息(效果如下图)。
1) Handle(Func
它为AggregateException中包含的每个异常都调用一个回调方法。然后,回调方法可以为每个异常决定如何对其进行处理,回调返回true表示异常已经处理,返回false表示没有。在调用Handle之后,如果至少有一个异常没有处理,就创建一个新的AggregateException对象,其中只包含未处理的异常,并抛出这个新的AggregateException对象。
比如:将任何OperationCanceledException对象都视为已处理。其他任何异常都造成抛出一个新的AggregateException,其中只包含未处理的异常。
try{……} catch (AggregateException ae) { ae.Handle(e => e is OperationCanceledException); }
1. 父任务生成了多个子任务,而多个子任务都抛出了异常
1) 嵌套子任务
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
对于子任务返回的异常类型为包装过的AggregateException对象,为了避免循环访问子任务异常对象的InnerExceptions才能获取真真的异常信息,可以使用上面提到的Flatten() 方法移除所有嵌套的AggregateExceprion。
2) Continue子任务
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
2. TaskScheduler的UnobservedTaskException事件
假如你一直不访问Task的Wait()、Result、Exception成员,那么你将永远注意不到这些异常的发生。为了帮助你检测到这些未处理的异常,可以向TaskScheduler对象的UnobservedTaskException事件注册回调函数。每当一个Task被垃圾回收时,如果存在一个没有注意到的异常,CLR的终结器线程会引发这个事件。
可在事件回调函数中调用UnobservedTaskExceptionEventArgs对象的SetObserved() 方法来指出已经处理好了异常,从而阻止CLR终止线程。然而并不推荐这么做,宁愿终止进程也不要带着已经损坏的状态继续运行。
示例代码:(要监控此代码必须在GC.Collect();和事件里两个地方进行断点)
TaskScheduler.UnobservedTaskException += (s, e) => { //设置所有未觉察异常已被处理 e.SetObserved(); }; Task.Factory.StartNew(() => { throw new Exception(); }); //确保任务完成 Thread.Sleep(100); //强制垃圾会受到,在GC回收时才会触发UnobservedTaskException事件 GC.Collect(); //等待终结器处理 GC.WaitForPendingFinalizers();
3. 返回void的async“异步方法”中的异常
我们已经知道返回Task或Task
然而对于返回void的“异步方法”,方法中抛出的异常会直接导致程序奔溃。
public static async void Test_void_async_Exception() { throw new Exception(); }
另外,我们还要特别注意lambda表达式构成的“异步方法”,如:
Enumerable.Range(0, 3).ToList().ForEach(async (i) => { throw new Exception(); });
本博文到此结束,我相信你看累了,其实我也写了很久…很久…,写完此文,我的“异步编程系列”也算有头有尾了(还会继续扩充)。本博文主要介绍了Task的重要API、任务的CLR线程池引擎、TaskFactory对象、TaskScheduler对象、TaskExtensions对象、AMP转化为TAP和EAP转化为TAP、使用关键字async和await实现异步方法以及自定义类型支持等待、处理TAP中的异常。
感谢你的观看,如果对你有帮助,还请多多推荐……
===================================================================
本篇博文基于.NET4.5中TPL所写。对于.NET4.0中TPL会有些差异,若有园友知道差异还请告知,我这边做个记录方便大家也方便自己。
1、.NET4.0中TPL未观察到的异常会在GC回收时终止进程。(园友:YamatAmain,讨论见21-26楼)
===================================================================
推荐阅读:
异步性能:了解 Async 和 Await 的成本-----有讲解到使用Task.ConfigureAwait(false)来避免捕获原上下文来提升性能。
关于async与await的FAQ -----详细讲解了await和async的作用和意义,以及什么是可等待对象、等待者……(此文可帮助你解决80%关于await和async关键字的疑惑)
深入探究 WinRT 和 await -----基于WinRT平板win8系统,讲解了异步功能API、TAP、编译器转换……
参考资料:MSDN
书籍:《CLR via C#(第三版)》
书籍:《C# 并行编程高级教程:精通.NET 4 Parallel Extensions》