[译]FAQ on Task.Start
Task的API设计确实有很多令人迷惑的地方,所以真的非常感谢Stephen Toub的这篇FAQ。节译其中的主要部分。
1.Q:我应该什么时候使用Task.Start?
A:Start实例方法可以用在且只能用在task处在Created状态(也就是task.Status返回TaskStatus.Created),而唯一能使task处在这一状态的方式就是使用Task的public构造器
2.Q:我有一个由Task.Run / Task.ContinueWith / Task.Factory.StartNew / TaskCompletionSource / 异步方法创建的task,我能不能对其调用Start方法?
A:如上所述,不能。会抛异常的。以上任意一个方法创建的task都意味着task不再处在Created状态,而是其他状态例如TaskStatus.WaitingForActivation、TaskStatus.Running、TaskStatus.RanToCompletion
3.Q:Start方法实际上做了什么?
A:它把task放入目标TaskScheduler(无参的Start方法意味着目标TaskScheduler为TaskScheduler.Current)。当你使用构造器创建一个task,它处于非活动状态,也就是说没有被放入任何一个TaskScheduler,因而没人会运行它。如果你不调用Start方法,它永远不会被放入队列,所以永远不会完成。想让task被执行,就需要把它放入队列,这样调度器会在它认为合适的时机运行你的task。Start方法会对task对象做一些变化(例如修改状态为WaitingToRun),之后调用目标TaskScheduler的QueueTask方法将其放入队列,从此时开始,task未来的运行就交由调度器处理,调度器最终会调用TryExecuteTask方法执行你的task。
4.Q:我能对同一个task多次调用Start方法么?
A:不能。一个task从Created状态变到别的状态这个行为只能出现一次,Start方法就会导致这个行为,所以Start方法也只能被调用一次。多次调用只会导致异常。Start方法引入了同步机制来保证task对象处在一致性的状态,所以即使多次并发的调用Start方法,也只有其中的一次调用会成功。
5.Q:Task.Start和Task.Factory.StartNew有什么不同?
A:Task.Factory.StartNew是一种简略的写法,实际就是构造一个task然后Start,所以如下的代码
var t = Task.Factory.StartNew(someDelegate);
和如下的代码从效果上是相同的
var t = new Task(someDelegate);
t.Start();
但从性能上来说,前者更好。如#3中所说,Start方法引入了同步机制来保证该task在Start之前尚未Start,也不会被并发的多次Start。与之相对的,StartNew方法知道不可能有其他人Start这个task因为这个引用尚未返回给任何人,所以StartNew方法无需引入同步机制。
6.Q:我听说调用Task.Result也可以开始一个task?
A:没这回事。当一个task处在Created状态,只有两种可能性使其变化到其他状态。
1. 构造task时传入了一个CancellationToken,当task还在Created状态时有了取消请求,那么task会变化为Canceled状态。
2. 有人调用了Start方法
调用Result不属于上述两种之一。当你对一个还处在Created状态的task调用Wait方法或者Result,调用会被阻塞。直至有人调用了Start,之后balabala,最终task完成了,被阻塞的调用才会完成。
你以为调用Result可能会开始一个task,那是不对的。实际上那倒有可能导致task被“内联”的执行。当task已经被放入TaskScheduler的队列,它也许并未开始,依然躺在队列里,当你对这种task调用Result,运行时可能会尝试内联的执行这个task(也就是说在你的调用线程上执行task),而不会等某个其他线程啥时候闲下来有空。从内部来说,就是Result内部也许会调用TaskScheduler的TryExecuteTaskInline方法,当然以上所说的“也许”都取决于TaskScheduler想怎么执行你的请求。
7. Q:我设计了一个公开的API要返回一个task,我应该返回一个尚未开始的task么?
A:不要这么做。(这里把这个问题和#1、#2区别对待是因为#1和#2是真的想创建一个task之后可能选择不开始它。大家不要以为非得违背设计意图的开始一个不想开始的task之后返回)
那么,通常的来讲如果你调用的是一个同步方法,那个方法肯定是你一调用就开始执行的。那么在返回一个task对象的异步方法的场合,你可以认为task就是未来终将会完成的异步方法,那并不改变一个事实:通过这个方法的调用,相应的操作应该已经开始执行了。因此,如果你返回一个尚未开始的task从语义上来讲就很奇怪。
所以如果你在方法内部使用构造器创造了一个task,那么在返回它之前记得调用Start,否则可能引起死锁或者类似的情况,因为消费者会期待着你返回的task终将完成,但如果你根本没开始这个task那它当然也就无从完成。有些框架甚至允许你通过一个方法或者委托神马的来指定一个检查,当你启用了这个检查,框架会检查返回的task的状态,如果尚处在Created状态会抛异常。
8. Q:说了半天我到底应不应该首先调用构造器创造一个task然后调Start呢?
A:对于大多数场合,你蛮好还是直接调用Task.Run或者TaskScheduler.StartNew吧,如果你的需求仅仅是让调度器帮你异步执行一段委托。这样不仅代码少,性能也好(如#5所述),而且减少了出错的几率(比如你忘了调Start神马的)。
当然构造器+Start方法也有其用武之地。比如你派生了自己的Task类型,那你就需要Start方法来真正把你的对象放入队列。再来看一个高级点例子:如果你想要在task的委托里使用这个task对象
Task theTask = null;
theTask = Task.Run(() => Console.WriteLine(“My ID is {0}.”, theTask.Id));
发现破绽了么?这儿有个竞态。当你调用Task.Run时,一个新的task对象被创建出来并被放入TaskScheduler的队列,如果当时线程池很闲,也许立刻就会有个线程来执行这段委托,这个线程会访问由主线程创建的theTask对象,从而存在一个竞态。想避免这种情况,只要把构造和开始分开执行就好了
Task theTask = null;
theTask = new Task(() =>Console.WriteLine(“My ID is {0}.”, theTask.Id));
theTask.Start(TaskScheduler.Default);
现在就可以保证在委托线程访问theTask对象之前主线程已经完成了对它的赋值