.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会改变其状态(从Created
到WaitingToRun
状态),然后将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的状态进行改变:
- 传递
CancellationToken
到Task的构造函数之中,并且CancellationToken
已经或者结束了请求。如果当Task仍然在Created
状态的时候前面的行为发生了,那么Task的状态会变为Canceled
的状态。 - 在该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
只是会调用TaskScheduler
的TryExecuteTaskInline()
方法,之后便完全取决于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.Run
和Task.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是不会进入调度的,所以就不会出现竞争的问题。
最新文章
- import sun.net.www.MimeTable报错
- [IOS基础]关于IOS的UIScreeen,UIView,UIViewController,UIWindow理解
- 【原创】C#玩高频数字彩快3的一点体会
- [软件测试]网站压测工具Webbench源码分析
- Ubuntu 创建开机自启动脚本的方法
- AIR 程序开发系列 之五 保存数据的几种方式
- onethink加密解密函数
- ExtJS 4 树
- canvas动画文字效果
- poj 2115 C Looooops(推公式+扩展欧几里得模板)
- 浅谈RSA加密
- 2017-10-31 中文代码示例教程之Vuejs入门&;后续计划
- CodeForces 623E Transforming Sequence 动态规划 倍增 多项式 FFT 组合数学
- Harry Potter and J.K.Rowling(半平面交+圆和矩形交)
- 工作中经常用到github上优秀、实用、轻量级、无依赖的插件和库
- python SMTP other
- 【10】python窗口控制[隐藏,移动]
- 原生js实现一个DIV的碰撞反弹运动
- PyQt4 QListWidget 使用教程
- 【实用代码片段】将json数据绑定到html元素 (转)