Task是一个更高级的抽象概念,它代表了一个并发操作,而该操作并不一定依赖线程来完成。Task是可以组合的(可以将它们通过延续(continuation)操作串联在一起)。它们可以使用线程池减少启动延迟,也可以通过TaskCompletionSource采用回调的方式避免多个线程同时等待I/O密集型操作。
Task类是Framework 4.0时作为并行编程库的组成部分引入的。然而它们后来经历了许多改进(通过使用等待器(awaiter)),从而在常见的并发场景中发挥了越来越大的作用。Task类也是C#异步功能的基础类型。
启动一个基于线程的Task的最简单方式是使用Task.Run
(Task类位于System.Threading.Tasks命名空间)静态方法。调用时只需传入一个Action委托:
Task.Run(()=> Console.WriteLine("foo"));
和下面的方式很相似:
new Thread (()=> Console.WriteLine("foo")).Start();
Task.Run会返回一个Task对象,它可以用于监控任务的执行过程。这一点与Thread对象不同。我们可以使用Task的Status属性来追踪其执行状态。
调用Task的Wait
方法可以阻塞当前方法,直到任务完成,这和调用线程对象的Join
方法类似:
Task task = Task.Run (() => { Console.WriteLine ("Task started"); Thread.Sleep (2000); Console.WriteLine ("Foo"); }); Console.WriteLine (task.IsCompleted); // False task.Wait();
可以在Wait中指定一个超时时间和取消令牌(可选)来提前终止等待状态。
默认情况下,CLR会将任务运行在线程池线程上,这种线程非常适合执行短小的计算密集的任务。如果要执行长时间阻塞的操作(如上面的例子),则可以按照以下方式避免使用线程池线程:
Task task = Task.Factory.StartNew(()=>......,TaskCreationOptions.LongRunning);
Task有一个泛型子类Task<TResult>
,它允许任务返回一个值。如果在调用Task.Run时传入一个Func<TResult>委托(或者兼容的Lambda表达式)替代Action就可以获得一个Task<TResult>对象,通过查询Result属性就可以获得任务的返回值。如果当前任务还没有执行完毕,调用该属性会阻塞当前线程,直至任务结束。
Task<int> task = Task.Run(() => { Console.WriteLine("Foo"); return 3; }); int result = task.Result;
可以将Task
理解为一个“未来值”,它封装了Result并将在以后生效。
任务可以方便地传播异常,这和线程是截然不同的。因此,如果任务中的代码抛出一个未处理异常,那么调用Wait()或者访问Task
Task task = Task.Run (() => { throw null; }); try { task.Wait(); } catch (AggregateException aex) { if (aex.InnerException is NullReferenceException) Console.WriteLine ("Null!"); else throw; }
使用Task的IsFaulted和IsCanceled属性可以在不抛出异常的情况下检测出错的任务。如果IsCanceled为true,则说明任务抛出了OperationCanceledException;如果IsFaulted为true,则说明任务抛出了其他类型的异常,通过Exception属性可以了解该异常的信息。
延续会告知任务在完成后继续执行后续的操作。延续通常由回调方法实现,该方法会在操作完成后执行。
以下是计算素数的例子:
Task<int> primeNumberTask = Task.Run (() => Enumerable.Range (2, 3000000).Count (n => Enumerable.Range (2, (int)Math.Sqrt(n)-1).All (i => n % i > 0))); var awaiter = primeNumberTask.GetAwaiter(); awaiter.OnCompleted (() => { int result = awaiter.GetResult(); Console.WriteLine (result); });
调用任务的GetAwaiter
方法将返回一个awaiter
对象。这个对象的OnCompleted
方法告知先导任务(primeNumberTask)当它执行完毕(或者出现错误)时调用一个委托。将延续附加到一个已执行完毕的任务上是完全没有问题的,此时,延续的逻辑将会立即执行。
如果先导任务出现错误,则延续代码调用awaiter.GetResult()时会重新抛出异常。对于非泛型任务,GetResult的返回值为void,这个函数的用途完全是为了重新抛出异常。
另一种创建任务的方法是使用TaskCompletionSource
,这种任务并非那种需要执行启动操作并在随后停止的任务;而是在操作结束或出错时手动创建的“附属”任务。这非常适用于I/O密集型的工作:它不但可以利用任务所有的优点(能够传递返回值、异常或延续),而且不需要在操作执行期间阻塞线程。
TaskCompletionSource的用法很简单,直接进行实例化即可。它包含一个Task属性,返回一个Task对象。
下面的例子会在等待5秒钟后输出42:
var tcs = new TaskCompletionSource<int>(); new Thread (() => { Thread.Sleep (5000); tcs.SetResult (42); }).Start(); Task<int> task = tcs.Task; Console.WriteLine (task.Result);
它是Task类的一个静态方法,是Thread.Sleep的异步版本,不会造成阻塞。