.Net Task常见问题

最近尝试使用一下Task,但是使用过程中因为API的不熟悉碰到了很多问题,不清楚什么时间来调用Task.Start(),具体该怎么使用等等。

如下所描述的Task.Start()方法均为实例方法。

1. 什么时候使用Task.Start()方法?

Task的Start方法当且仅当Task的状态是Created状态的时候才能使用。而想让Task的状态是Created的状态的话,只要通过任何Task的构造函数即可,比如说var t = new Task(someDelegate);

Task的状态包含如下:

    public enum TaskStatus
    {
        Created,
        WaitingForActivation,
        WaitingToRun,
        Running,
        WaitingForChildrenToComplete,
        RanToCompletion,
        Canceled,
        Faulted
    }

Task的状态分为代码中显示的8种:
Created就是前面提到的,当实例化的时候,Task的状态就是Created状态。
WaitingForActivation代表着该Task已经被Start了,但是等待进入.Net的调度结构
WaitingToRun表示Task已经被调度了,只是还没有开始执行
Running表示该Task正在运行,但是还没有执行结束
WaitingForChildrenToComplete表示该Task其实已经完成了执行,但是等待其中附带的子Task执行结束
RanToCompletion表示Task成功执行完毕
Canceled表示任务执行时通过其自定义的CancellationToken发信号的时候抛出了OperationCancelledException,或者是Task的CancellationToken在任务执行之前就已经发送了信号
Faulted表示Task在执行过程中抛出了一个没有处理异常信息
从Task的状态我们可以知道,.Net的调度有些类似于线程调度器的作用,但是是基于线程的更具体的抽象,可以进行取消等操作。

2. 由Task.Run/Task.ContinueWith/Task.Factory.StartNew/TaskCompletionSource/异步方法等方法产生的Task,开发者是否需要调用Task.Start()方法?

答案是不需要。不仅仅是不应该,而是真的不能。。如前面的问题所提到的,Task仅仅在Created的状态才允许调用Task.Start()方法的,而通过提到的这些方法所创建的Task都已经不是Created的状态了,比如TaskStatus.WaitingForActivation, 或者 TaskStatus.Running, 或者TaskStatus.RanToCompletion等。

3. Task.Start()真正做了些什么?

当执行了Task.Start()之后,会将Task加入到TaskScheduler的队列之中(无参启动的话,队列为TaskScheduler.Current)。当开发者通过Task的构造函数构建了一个Task的时候,Task仍然是未激活状态,该Task仍然还没有开始调度,所以不会执行任何任务。如果不通过Task.Start()来启动任务,Task就永远不会进入队列,也永远不会完成。为了让Task能够顺利执行,开发者就必然要将其加入到调度队列之中,这样才能够让调度器在正常的时候进行执行任务。当执调用了Task.Start()的时候,Task会改变其状态(从CreatedWaitingToRun状态),然后将Task送到指定的TaskScheduler上进行调度。到了这个时候,Task后续的执行就被交给TaskScheduler来完成了,只会会通过TaskScheduler的TryExecuteTask方法来执行。

4. 在同一个Task能否多次调用Task.Start()?

答案是否。一个Task只能从Created状态转变一次,也就是说Task.Start()只能执行一次。当Task不再处于Created状态之后,任何的Task.Start()调用都会抛出异常。Task.Start()方法通过同步来确保Task对象能够保持一致的状态,是否并发执行都不会有线程安全问题。

5. Task.Start()Task.Factory.StartNew()之间有什么区别?

Task.Factory.StartNew()相当于new一个Task并调用Task.Start()的简写版本,代码如下:

// Task.Factory.StartNew()
var t = Task.Factory.StartNew(someDelegate);

// equivalent to 
var t = new Task(someDelegate); 
t.Start();

从性能的角度来说的话,前面的方式会更为高效一些。在第三个问题中描述过,当调用Task.Start()的时候有同步操作,来确保Task实例还没有启动,或者并发启动。相对的,在Task.Factory.StartNew()放阿飞中,.Net可以确定没有人并发的执行启动Task,所以不需要进行同步操作。

6. Task.Result是否可能也会启动Task?

答案是否。有且仅有两种方式来令一个处于Created状态的Task的状态进行改变:

  1. 传递CancellationToken到Task的构造函数之中,并且CancellationToken已经或者结束了请求。如果当Task仍然在Created状态的时候前面的行为发生了,那么Task的状态会变为Canceled的状态。
  2. 在该Task上面调用Task.Start()方法。

想令一个Task的状态从Created转变,就只有这两种方式。如果开发者在处于Created状态的Task上面使用Task.Wait()或者Task.Result方法的话,调用将会阻塞;只有其他地方调用了Task.Start()的时候才能将其加入TaskScheduler的队列之中进行调度,这样Task才可能完成,前面阻塞的调用才能被唤醒。

Task.Result方法不能启动Task,但是可能潜在的改变TaskScheduler针对Task的执行顺序。如果Task已经被TaskScheduler调度了,那么Task本身可能还在等待执行。当开发调用Task.Result方法时,运行时可能会尝试内联任务的执行(意味着在线程上执行任务),而不是会让TaskScheduler阻塞其他线程的执行来立即执行调用Task.Result的任务。执行了Task.Result只是会调用TaskSchedulerTryExecuteTaskInline()方法,之后便完全取决于TaskScheduler是如何调度了。

7. 开发者是否该从public API返回一个没有执行Start的Task?

实际的问题应该是,“我是否该返回一个状态为Created的Task?” 而答案很明显,就是不行。

基本的思想如下:当开发者调用一个普通的同步的方法时,调用会在调用的一刻就立即执行。但是对于那些返回Task的方法,我们可以认为哪个Task意味着方法的异步完成。但是这并不影响调用其相关的Start之类的操作。因此,如果返回一个处于Created状态的Task是很奇怪的,也表示Task并没有启动。

所以,如果开发者需要在一个公开的方法返回一个Task,那么,请在返回之前,调用Task的构造函数之后就直接Start这个Task。否则,你返回的Task没有执行的话,很容易产生一个死锁或者类似的问题。因为调用方可能在等待Task执行的完成来继续其他的操作的,而Task还没有开始执行,那么就永远不会结束的。一些框架的允许开发者来通过方法和回调来参数化框架,其返回的Task还会验证Task的状态的,如果返回的Task是Created的状态,就会抛出异常。

8. 开发者是否该使用Task的构造函数和Task.Start()方法?

在绝大多数的情况下,开发者最好不要通过Task的构造函数和Task.Start这种机制。比如,如果开发者只是想调度一个任务来执行一些代理,那么最好使用Task.Run或者Task.Factory.StartNew。不仅仅是因为Task.RunTask.Factory.StartNew的代码更少,同样也是因为其性能更高(没有同步的代价),开发者也不容易出错,所以最好别使用Task的构造函数以及Task.Start()操作。

当然了,有很多场景使用Task的构造函数以及Task.Start()也是非常合理的。举例来说,如果开发者选择继承Task来做一些操作的话,那么开发者就需要使用Task.Start()方法来将其加入TaskScheduler的队列了。另一个例子就是,如果开发者想要使用Task本身的一些属性。比如如下的问题代码:

Task theTask = null; 
theTask = Task.Run(() => Console.WriteLine(“My ID is {0}.”, theTask.Id));

上面的代码会有瑕疵,那就是竞争。在调用Task.Run()的时候,会创建一个Task对象并且将其加入线程池的调度队列中,但是如果当前有足够的线程资源,会立刻从线程池中挑选线程来立刻创建Task并执行。那个线程会立刻进入主线程的变量theTask来调用Task.Run,但是创建的Task可能还没有写入到theTask变量,但是分开就可以解决这个问题,代码如下:

Task theTask = null; 
theTask = new Task(() =>Console.WriteLine(“My ID is {0}.”, theTask.Id)); 
theTask.Start(TaskScheduler.Default);

现在,我们就可以确定上面的代码不会抛出空指针异常,因为Task在执行前就已经写入了theTask变量之中,前面已经提到了,不执行Task.Start()的话,Task是不会进入调度的,所以就不会出现竞争的问题。

原文地址:https://www.cnblogs.com/qitian1/p/6461540.html