C# 异步编程基础(五)Task
此入门教程是记录下方参考资料视频的过程
开发工具:Visual Studio 2019
目录
C# 异步编程基础(六)Continuation 继续/延续 、TaskCompletionSource、实现 Task.Delay
C# 异步编程基础(十) 取消(cancellation)、进度报告、TAP(Task-Based Asynchronous Pattern)、Task组合器
线程(Thread)的问题
- 线程(Thread)是用来创建并发(concurrency)的一种低级别工具,它有一些限制,尤其是:
虽然开始线程的时候可以方便的传入数据,但是当Join的时候,很难从线程获得返回值
可能需要设置一些共享字段
如果操作抛出异常,捕获和传播该异常都很麻烦
无法告诉线程在结束时开始做另外的工作,你必须进行Join操作(在进程中阻塞当前的线程) - 很难使用较小的并发(concurrent)来组件大型的并发
- 导致了对手动同步的更大依赖以及随之而来的问题
Task class
- Task类可以很好的解决上述问题
- Task说一个相对高级的抽象:它代表了一个并发操作(concurrent)
该操作可能由Thread支持,或不由Thread支持 - Task是可组合的(可使用Continuation把它们串成链)
Tasks可以使用线程池来减少启动延迟
使用TaskCompletionSource,Tasks可以利用回调的方式,在等待I/O绑定操作时完全避免使用线程
开始一个Task Task.Run
- Task类在System.Threading.Tasks命名空间下
- 开始一个Task最简单的办法就是使用Task.Run(.NET 4.5,4.0的时候就是Task.Factory.StartNew)整个静态方法:
传入一个Action委托即可
例子
static void Main(string[] args)
{
Task.Run(()=>Console.WriteLine("Foo"));
}
- Task默认使用线程池,也就是后台线程:
当主线程结束时,你创建的所有tasks都会结束(例子task) - Task.Run返回一个Task对象,可以使用它来监视其过程
在Task.Run之后,我们没有调用Start,因为该方法创建的是“热”任务(hot task)
可以通过Task的构造函数创建“冷”任务(cold taks),但是很少这样做 - 可以通过Task的Status属性来跟踪task的执行状态
等待 Wait
- 调用task的Wait方法会进行阻塞直到操作完成
相当于调用thread上的Join方法
例子
static void Main(string[] args)
{
Task task=Task.Run(()=>{
Thread.Sleep(3000);
Console.WriteLine("Foo");
});
Console.WriteLine(task.IsCompleted);//fasle
task.Wait();//阻塞直到task完成操作
Console.WriteLine(task.IsCompleted);//true
}
- Wait也可以让你指定一个超时时间和一个取消令牌来提前结束等待,具体看 Help Viewer
长时间运行的任务 Long-running tasks
- 默认情况下,CLR在线程池中运行Task,这非常适合短时间运行的Compute-Bound类工作
- 针对长时间运行的任务或者阻塞操作(例如前面的例子),你可以不采用线程池
例子
static void Main(string[] args)
{
Task task=Task.Factory.StartNew(()=>
{
Thread.Sleep(3000);
System.Console.WriteLine("Foo");
},TaskCreationOptions.LongRunning);
}
- 如果同时运行多个long-running tasks(尤其是其中有处于阻塞状态的),那么性能将会受很大影响,这时有比TaskCreationOptions.LongRunning更好的办法:
如果任务是IO-Bound,TaskCompletionSource和异步函数可以让你用回调(Coninuations)代替线程来实现并发
如果任务是Compute-Bound,生产者/消费者队列允许你对任务的并发性进行限流,避免把其它线程和进程饿死
Task的返回值
- Task有一个泛型子类叫做Task
,它允许发出一个返回值 - 使用Func
委托或兼容的Lambda表达式来调用Task.Run就可以得到Task - 随后,可以通过Result属性来获得返回的结果
如果这个task还没有完成操作,访问Result属性会阻塞线程直到该task完成操作
例子
static void Main(string[] args)
{
Task<int> task=Task.Run(()=>
{
Console.WriteLine("Foo");
return 3;
});
//如果task没完成,那么就阻塞
int result=task.Result;
//3
Console.WriteLine(result);
}
例子,输出2到3000000中质数的个数
static void Main(string[] args)
{
Task<int> primeNumerTask = Task.Run(() =>
Enumerable.Range(2, 3000000).Count(n =>
Enumerable.Range(2, (int)Math.Sqrt(n) - 1).All(i => n % i > 0)));
Console.WriteLine("Task running");
Console.WriteLine("The answer is " + primeNumerTask.Result);
}
- Task
可以看作是一种所谓的“未来/许诺”(future、promise),在它里面包裹着一个Result,在稍后的时候就会变得可用 - 在CTP版本的时候,Task
实际上叫做Future
Task的异常
- 与Thread不一样,Task可以很方便的传播异常
如果你的task里面抛出了一个未处理的异常(故障),那么该异常就会重新抛出给:
调用了Wait()的地方
访问了Task的Result属性的地方
例子
static void Main(string[] args)
{
Task task=Task.Run(()=>{throw null;});
try
{
task.Wait();
}
catch(AggregateException aex)
{
if(aex.InnerException is NullReferenceException)
{
Console.WriteLine("Null");
}
else
{
throw;
}
}
}
- CLR将异常包裹在AggregateException里,以便在并行编程场景中发挥很好的作用
- 无需重新抛出异常,通过Task的IsFaulted和IsCanceled属性也可以检测出Task是否发生了故障:
如果两个属性都返回false,那么没有错误发生
如果IsCanceled为true,那就说明一个OperationCanceledException为该Task抛出了
如果IsFaulted为true,那就说明另一个类型的异常被抛出了,而Exception属性也将指明错误
异常与“自治”的Task
- 自治的,“设置完就不管了”的Task。就是指不通过调用Wait()方法、Result属性或continuation进行会合的任务
- 针对自治的Task,需要像Thread一样,显式的处理异常,避免发生“悄无声息的故障”
- 自治Task上未处理的异常称为未观察到的异常
未观察到的异常
- 可以通过全局的TaskScheduler.UnobservedTaskException来订阅未观察到的异常
- 关于什么是“未观察到的异常”,有一些细微的差别:
使用超时进行等待的Task,如果在超时后发生故障,那么它将会产生一个“未观察到的异常”。
在Tsk发生故障后,如果访问Task的Exception属性,那么该异常就被认为是“已观察到的”。