(译).NET4.X 并行任务中Task.Start()的FAQ
近期有不少人向我咨询关于Task的Start()方法。比如:何时使用及何时不使用Start()、Start()又做了些什么……我想在这里回答一些问题试图澄清和平息任何关于Start()方法是什么以及做了什么的误解。
1. 问题:我什么时候能使用Task的Start()方法?
只有Task处于TaskStatus.Created状态时才能使用实例方法Start()。并且,只有在使用Task的公共构造函数构造的Task实例才能处于TaskStatus.Created状态。
表示 Task 的生命周期中的当前阶段。
public enum TaskStatus { // 该任务已初始化,但尚未被计划。 Created = 0, // 该任务正在等待 .NET Framework 基础结构在内部将其激活并进行计划。 WaitingForActivation = 1, // 该任务已被计划执行,但尚未开始执行。 WaitingToRun = 2, // 该任务正在运行,但尚未完成。 Running = 3, // 该任务已完成执行,正在隐式等待附加的子任务完成。 WaitingForChildrenToComplete = 4, // 已成功完成执行的任务。 RanToCompletion = 5, // 该任务已通过对其自身的 CancellationToken 引发 OperationCanceledException 异常 Canceled = 6, // 由于未处理异常的原因而完成的任务。 Faulted = 7, }
2. 问题:使用Task.Run()/Task.ContinueWith()/Task.Factory.StartNew()/TaskCompletionSource/异步方法(即使用async与await关键字的方法)……应该调用Start()方法吗?
不应该。不仅不应该,而且也不能,因为此时调用Start()会报异常。从问题1可知:Start()实例方法只适用于TaskStatus.Created状态的Task。由上面提到的方式创建的Task其状态不是TaskStatus.Created,而是如TaskStatus.WaitingForActivation、TaskStatus.Running或TaskStatus.RanToCompletion。
3. 问题:Start()方法实际做了什么?
Start()将任务排队到目标TaskScheduler(无参的Start()重载任务调度者为TaskScheduler.Current)。当你使用Task的构造函数创建一个Task实例时,它处于创建状态(TaskStatus.Created),它没有与任何调度器关联,也没有真真被执行。如果你永远不调用Start()方法,那么此任务永远不会排队也不会完成。为了让任务被执行,它需要在调度器上进行排队,以便调度器在合适的时刻执行它。在Task上调用Start()方法将改变任务内部的一些数据(eg:状态从Created改变为WaitingToRun)并且将任务通过TaskScheduler实例的QueueTask()方法排队到目标调度器。此时,此任务未来的执行掌握在调度器手中,最终会通过TaskScheduler的TryExecuteTask实例方法执行。
// 表示一个处理将任务排队到线程中的底层工作的对象。 public abstract class TaskScheduler { protected TaskScheduler(); // 获取与当前正在执行的任务关联的 TaskScheduler。 public static TaskScheduler Current { get; } // 获取由 .NET Framework 提供的默认 TaskScheduler 实例。 public static TaskScheduler Default { get; } // 创建一个与当前 SynchronizationContext 关联的 TaskScheduler。 public static TaskScheduler FromCurrentSynchronizationContext(); // 当出错的 Task 的未观察到的异常将要触发异常升级策略时发生,默认情况下,这将终止进程。 public static event EventHandler<UnobservedTaskExceptionEventArgs> UnobservedTaskException; // 获取此 TaskScheduler 实例的唯一 ID。 public int Id { get; } // 指示此 TaskScheduler 能够支持的最大并发级别,默认计划程序返回 System.Int32.MaxValue。 public virtual int MaximumConcurrencyLevel { get; } // 仅对于调试器支持,生成当前排队到计划程序中等待执行的 Task 实例的枚举。 protected abstract IEnumerable<Task> GetScheduledTasks(); // 将 Task 排队到计划程序中。 protected internal abstract void QueueTask(Task task); // 尝试将以前排队到此计划程序中的 Task 取消排队。 protected internal virtual bool TryDequeue(Task task); // 尝试在此计划程序上执行提供的 Task。执行失败的常见原因是, // 该任务先前已经执行或者位于正在由另一个线程执行的进程中。 protected bool TryExecuteTask(Task task); // 确定提供的 Task是否可以在此调用中同步执行,如果可以,将执行该任务。 // taskWasPreviouslyQueued: // 一个布尔值,该值指示任务之前是否已排队。如果此参数为 True,则该任务以前可能已排队(已计划); // 如果为 False,则已知该任务尚未排队,此时将执行此调用,以便以内联方式执行该任务,而不用将其排队。 // 返回结果: // 一个布尔值,该值指示是否已以内联方式执行该任务。 protected abstract bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued); }
4. 问题:我能在同一个Task上调用多次Start()方法吗?
不行,Task只能离开创建状态(TaskStatus.Created)一次,而Start()方法使Task离开创建状态。因此,Start()方法只能使用一次。任何企图在不是创建状态的Task上调用Start()都将导致一个异常。Start()方法采用同步方式运行以确保任务对象保持一致的状态,即使是同时调用多次Start(),也只可能有一个调用会成功。
5. 问题:使用Task.Start()与使用Task.Factory.StartNew()有何不同?
Task.Factory.StartNew()可快速创建一个Task并且开启任务。代码如下:
var t = Task.Factory.StartNew(someDelegate);
这等效于:
var t = new Task(someDelegate); t.Start();
表现方面,前者更高效。就像在问题3中提到的,Start()采用同步方式运行以确保任务对象保持一致的状态即使是同时调用多次Start(),也可能只有一个调用会成功。相比之下,StartNew()知道没有其他代码能同时启动任务,因为在StartNew()返回之前它不会将创建的Task引用给任何人,所以StartNew()不需要采用同步方式执行。
6. 问题:我听说Task.Result属性也能开启任务,真的吗?
不能,只有两种方式可能使Task离开其创建状态:
1) 将一个CancellationToken传递给Task的构造函数,并且这个token已经或稍后请求取消。如果Task任然处于Created/WaitingForActivation/WaitingToRun,当token取消时,Task将改变为TaskStatus.Canceled状态。
2) 在Task上调用Start()。
因此, Result属性不能开启任务。如果对处于创建状态的Task实例上调用Wait()方法或Result属性,这个调用将被阻塞,需要等待开启任务,这样它就可以排队到调度器,调度程序最终会执行它,然后完成任务。被阻塞的调用就被唤醒。
你可能会认为不是这样的。Result能开启任务,但是这只适用于“内联”任务的执行。如果一个任务已经排队到TaskScheduler,但是这个任务可能任然保持在调取器的任务队列中。当你对被排队的任务请求Result属性时,运行时将尝试内联任务的执行而不是纯粹的阻塞和等待调度器在未来某个时刻使用其他线程来完成任务执行。因此,调用Result属性可能终止于TaskScheduler的TryExecuteTaskInline()方法的调用,并且如何处理请求由TaskScheduler决定。
7. 问题:我应该提供返回未启动任务的公共APIs吗?
更恰当的问题是“我应该提供处于创建状态任务的公共APIs吗”,答案是“不能”。
基本原因是:当你正常调用同步方法,该方法将很快被调用执行。对于返回Task的方法,你可以把Task看做是异步方法完成的结果。但是这并不能改变需要调用Start()方法开始相关操作的事实。因此,返回一个处于创建状态的Task的异步方法是很奇怪的,只是想代表一个没有开始的操作?
因此,如果你有一个返回Task的公共方法,并且Task是使用构造函数创建的,请确保你在返回Task之前开启任务。否则,很可能在APIs使用方导致死锁或类似的问题,因为使用方期待调用完成时Task也最终完成,但如果返回一个尚未启动的任务,它将永远不会完成。有些框架允许你参数化方法或委托,返回Task甚至验证返回任务的状态,如果Task还是创建状态就为其抛出异常。
8. 问题:我应该使用Task的构造函数 + Task的Start()实例方法吗?
在大多数情况下,你最好使用一些其他机制。比如,如果你只是想计划一个任务来运行你提供的委托,你最好使用Task.Run()静态方法或Task.Factory的StartNew()实例方法,而不是使用Task的构造函数创建一个任务再调用Start()开启它,这不仅仅减少了代码量,并且更加高效(见问题5回答),另外还可以减少犯错的可能,比如忘记开启任务。
当然,在一些情况下使用Task的构造函数+Start()更加有意义。比如,需要根据某些原因来选择传递而来的Task,然后再使用Start()方法来实际排队任务。
另外,一个更明显的示例是,如果你想获得任务本身的引用,可能使用了如下代码:
Task theTask = null; theTask = Task.Run(() => Console.WriteLine(“My ID is {0}.”, theTask.Id));
有问题,存在竞争?在Task.Run()方法内部,会创建一个新Task对象并且将其排队到线程池调度器中。如果线程池比较空闲,那么会立即分配一个辅助线程开始执行任务。新创建的任务最后会存储在theTask变量中,辅助线程和调用Task.Run()的线程会发生竞争的访问此变量。我们能解决这种竞争通过分离构造函数与TaskScheduler:
Task theTask = null; theTask = new Task(() =>Console.WriteLine(“My ID is {0}.”, theTask.Id)); theTask.Start(TaskScheduler.Default);
现在我们已经确保Task实例将在线程池执行任务之前被存储到theTask变量。因为线程池在Task对象调用Start()排队任务之前无法获得Task对象的引用,并且在这个时候,变量theTask已经设置为Task的引用,与后面线程池访问theTask.Id不会存在竞争问题。
推荐并行任务相关资源:《关于Async与Await的FAQ》
================================================================================================
园友提醒:(.NET4.5对.NET4.0的并行任务进行过改进,然而我正式学习并行任务的时候已经是.NET4.5,所以对于新改进的API没有进行整理了,这边有园友提醒,做下记录,方便大家。)
@zhangweiwen(Task.Run()是.net4.5新提供的API)
================================================================================================
Ok,看完此文,相信你对并行任务中关于任务开启又有更深入的理解了,(*^_^*),喜欢还请多多推荐。
原文:http://blogs.msdn.com/b/pfxteam/archive/2012/01/14/10256832.aspx
作者:Stephen Toub
作者:滴答的雨
出处:http://www.cnblogs.com/heyuquan/
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。
欢迎园友讨论下自己的见解,及向我推荐更好的资料。
本文如对您有帮助,还请多帮 【推荐】 下此文。
谢谢!!! (*^_^*)
技术群:(339322839广西IT技术交流),欢迎你的加入