我们在之前的章节中学习了什么是线程,如何使用线程,以及为什么需要线程池。使用线程池可以使我们在减少并行度花销时节省操作系统资源。我们可以认为线程池是一个抽象层,其向程序员隐藏了使用线程的细节,使我们专心处理程序逻辑,而不是各种线程,问题。
然而使用线程池也相当复杂。从线程池的工作线程中获取结果并不容易。我们需要实现,自定义方式来获取结果,而且万一有异常发生,还需将异常正确地传播到初始线程中。除此,以外,创建一组相关的异步操作,以及实现当前操作执行完成后下一操作才会执行的逻辑也不容易。在尝试解决这些问题的过程中,创建了异步编程模型及基于事件的异步模式。在第3章中提到过基于事件的异步模式。这些模式使得获取结果更容易,传播异常也更轻松,但是组,合多个异步操作仍需大量工作,需要编写大量的代码。
为了解决所有的问题, .Net Framework4.0引入了一个新的关于异步操作的API,它叫做.任务并行库( Task Parallel Library,简称TPL), .Net Framework 4.5版对该API进行了轻微的改进,使用更简单。在本书的项目中将使用最新版的TPL,即.Net Framework 4.5版中的 API, TPL可被认为是线程池之上的又一个抽象层,其对程序员隐藏了与线程池交互的底层代码,并提供了更方便的细粒度的APL, TPL的核心概念是任务。一个任务代表了一个异步操作,该操作可以通过多种方式运行,可以使用或不使用独立线程运行。在本章中将探究任务的所有使用细节。
默认情况下,程序员无须知道任务实际上是如何执行的。TPL通过向用户隐藏任务的实现细节从而创建一个抽象层。遗憾的是,有些情况下这会导致诡秘的错误,比如试图获取任务的结果时程序被挂起。本章有助于理解TPL底层的原理,以及如何避免不恰当的使用方式。
一个任务可以通过多种方式和其他任务组合起来。例如,可以同时启动多个任务,等待所有任务完成,然后运行一个任务对之前所有任务的结果进行一些计算。TPL与之前的模式相比,其中一个关键优势是其具有用于组合任务的便利的API,
处理任务中的异常结果有多种方式。由于一个任务可能会由多个其他任务组成,这些任,务也可能依次拥有各自的子任务,所以有一个AggregateException的概念。这种异常可以捕获底层任务内部的所有异常,并允许单独处理这些异常。
而且,最后但并不是最不重要的, C# 5.0已经内置了对TPL的支持,允许我们使用新的 await和async关键字以平滑的、舒服的方式操作任务。
在本章中我们将学习使用TPL来执行异步操作。我们将学习什么是任务,如何用不同的,方式创建任务,以及如何将任务组合在一起。我们会讨论如何将遗留的APM和EAP模式转换为使用任务,还有如何正确地处理异常,如何取消任务,以及如何使多个任务同时执行。另外,还将讲述如何在Windows GUI应用程序中正确地使用任务。
class Program { static void Main(string[] args) {
//使用Task构造函数创建了两个任务[只有调用了这些任务的Start方法,Task才会执行任务] var t1 = new Task(() => TaskMethod("Task 1")); var t2 = new Task(() => TaskMethod("Task 2")); t2.Start(); t1.Start();
//与使用Task构造函数的不同=======Task.Run和Task.StartNew方法创建的任务会立即开始执行,无需显示调用Start方法; Task.Run(() => TaskMethod("Task 3")); Task.Factory.StartNew(() => TaskMethod("Task 4"));
//Task.Run方法只是Task.Factory.StartNew的一个快捷方式,但是后者有附加的选项。
//我们标记该任务为长时间运行,结果该任务将不会使用线程池,而在单独的线程中运行 Task.Factory.StartNew(() => TaskMethod("Task 5"), TaskCreationOptions.LongRunning); Thread.Sleep(TimeSpan.FromSeconds(1)); Console.ReadKey(); } static void TaskMethod(string name) { Console.WriteLine("Task {0} is running on a thread id {1}. Is thread pool thread: {2}", name, Thread.CurrentThread.ManagedThreadId, Thread.CurrentThread.IsThreadPoolThread); } }
当程序运行时,我们使用Task的构造函数创建了两个任务。我们传入一个lambda表达式作为Action委托。这可以使我们给TaskMethod提供一个string参数。然后使用Start方法运行这些任务。
请注意只有调用了这些任务的Start方法,才会执行任务。很容易忘记真正启动任务。
然后使用Task.Run和Task.Factory.StartNew方法来运行了另外两个任务。与使用Task构造函数的不同之处在于这两个被创建的任务会立即开始工作,所以无需显式地调用这些任务的Start方法。从Task 1到Task 4的所有任务都被放置在线程池的工作线程中并以未指定的顺序运行。如果多次运行该程序,就会发现任务的执行顺序是不确定的。
Task.Run方法只是Task.Factory.StartNew的一个快捷方式,但是后者有附加的选项。通!常如果无特殊需求,则可使用前一个方法,如Task 5所示。我们标记该任务为长时间运行,结果该任务将不会使用线程池,而在单独的线程中运行。然而,根据运行该任务的当前的任务调度程序( task scheduler)运行方式有可能不同。